##// END OF EJS Templates
merge with stable
Augie Fackler -
r44232:29adf0a0 merge default
parent child Browse files
Show More
@@ -1,189 +1,190 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,202 +1,203 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 a50fecefa691c9b72a99e49aa6fe9dd13943c2bf 5.2.1
@@ -1,1261 +1,1261 b''
1 1 # githelp.py - Try to map Git commands to Mercurial equivalents.
2 2 #
3 3 # Copyright 2013 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 """try mapping git commands to Mercurial commands
8 8
9 9 Tries to map a given git command to a Mercurial command:
10 10
11 11 $ hg githelp -- git checkout master
12 12 hg update master
13 13
14 14 If an unknown command or parameter combination is detected, an error is
15 15 produced.
16 16 """
17 17
18 18 from __future__ import absolute_import
19 19
20 20 import getopt
21 21 import re
22 22
23 23 from mercurial.i18n import _
24 24 from mercurial import (
25 25 encoding,
26 26 error,
27 27 fancyopts,
28 28 pycompat,
29 29 registrar,
30 30 scmutil,
31 31 )
32 32 from mercurial.utils import procutil
33 33
34 34 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
35 35 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
36 36 # be specifying the version(s) of Mercurial they are tested with, or
37 37 # leave the attribute unspecified.
38 38 testedwith = b'ships-with-hg-core'
39 39
40 40 cmdtable = {}
41 41 command = registrar.command(cmdtable)
42 42
43 43
44 44 def convert(s):
45 45 if s.startswith(b"origin/"):
46 46 return s[7:]
47 47 if b'HEAD' in s:
48 48 s = s.replace(b'HEAD', b'.')
49 49 # HEAD~ in git is .~1 in mercurial
50 50 s = re.sub(b'~$', b'~1', s)
51 51 return s
52 52
53 53
54 54 @command(
55 55 b'githelp|git',
56 56 [],
57 57 _(b'hg githelp'),
58 58 helpcategory=command.CATEGORY_HELP,
59 59 helpbasic=True,
60 60 )
61 61 def githelp(ui, repo, *args, **kwargs):
62 62 '''suggests the Mercurial equivalent of the given git command
63 63
64 64 Usage: hg githelp -- <git command>
65 65 '''
66 66
67 67 if len(args) == 0 or (len(args) == 1 and args[0] == b'git'):
68 68 raise error.Abort(
69 69 _(b'missing git command - usage: hg githelp -- <git command>')
70 70 )
71 71
72 72 if args[0] == b'git':
73 73 args = args[1:]
74 74
75 75 cmd = args[0]
76 76 if not cmd in gitcommands:
77 77 raise error.Abort(_(b"error: unknown git command %s") % cmd)
78 78
79 79 ui.pager(b'githelp')
80 80 args = args[1:]
81 81 return gitcommands[cmd](ui, repo, *args, **kwargs)
82 82
83 83
84 84 def parseoptions(ui, cmdoptions, args):
85 85 cmdoptions = list(cmdoptions)
86 86 opts = {}
87 87 args = list(args)
88 88 while True:
89 89 try:
90 90 args = fancyopts.fancyopts(list(args), cmdoptions, opts, True)
91 91 break
92 92 except getopt.GetoptError as ex:
93 93 if "requires argument" in ex.msg:
94 94 raise
95 95 if ('--' + ex.opt) in ex.msg:
96 96 flag = b'--' + pycompat.bytestr(ex.opt)
97 97 elif ('-' + ex.opt) in ex.msg:
98 98 flag = b'-' + pycompat.bytestr(ex.opt)
99 99 else:
100 100 raise error.Abort(
101 101 _(b"unknown option %s") % pycompat.bytestr(ex.opt)
102 102 )
103 103 try:
104 104 args.remove(flag)
105 105 except Exception:
106 106 msg = _(b"unknown option '%s' packed with other options")
107 107 hint = _(b"please try passing the option as its own flag: -%s")
108 108 raise error.Abort(
109 109 msg % pycompat.bytestr(ex.opt),
110 110 hint=hint % pycompat.bytestr(ex.opt),
111 111 )
112 112
113 113 ui.warn(_(b"ignoring unknown option %s\n") % flag)
114 114
115 115 args = list([convert(x) for x in args])
116 116 opts = dict(
117 117 [
118 (k, convert(v)) if isinstance(v, str) else (k, v)
118 (k, convert(v)) if isinstance(v, bytes) else (k, v)
119 119 for k, v in pycompat.iteritems(opts)
120 120 ]
121 121 )
122 122
123 123 return args, opts
124 124
125 125
126 126 class Command(object):
127 127 def __init__(self, name):
128 128 self.name = name
129 129 self.args = []
130 130 self.opts = {}
131 131
132 132 def __bytes__(self):
133 133 cmd = b"hg " + self.name
134 134 if self.opts:
135 135 for k, values in sorted(pycompat.iteritems(self.opts)):
136 136 for v in values:
137 137 if v:
138 138 if isinstance(v, int):
139 139 fmt = b' %s %d'
140 140 else:
141 141 fmt = b' %s %s'
142 142
143 143 cmd += fmt % (k, v)
144 144 else:
145 145 cmd += b" %s" % (k,)
146 146 if self.args:
147 147 cmd += b" "
148 148 cmd += b" ".join(self.args)
149 149 return cmd
150 150
151 151 __str__ = encoding.strmethod(__bytes__)
152 152
153 153 def append(self, value):
154 154 self.args.append(value)
155 155
156 156 def extend(self, values):
157 157 self.args.extend(values)
158 158
159 159 def __setitem__(self, key, value):
160 160 values = self.opts.setdefault(key, [])
161 161 values.append(value)
162 162
163 163 def __and__(self, other):
164 164 return AndCommand(self, other)
165 165
166 166
167 167 class AndCommand(object):
168 168 def __init__(self, left, right):
169 169 self.left = left
170 170 self.right = right
171 171
172 172 def __str__(self):
173 173 return b"%s && %s" % (self.left, self.right)
174 174
175 175 def __and__(self, other):
176 176 return AndCommand(self, other)
177 177
178 178
179 179 def add(ui, repo, *args, **kwargs):
180 180 cmdoptions = [
181 181 (b'A', b'all', None, b''),
182 182 (b'p', b'patch', None, b''),
183 183 ]
184 184 args, opts = parseoptions(ui, cmdoptions, args)
185 185
186 186 if opts.get(b'patch'):
187 187 ui.status(
188 188 _(
189 189 b"note: Mercurial will commit when complete, "
190 190 b"as there is no staging area in Mercurial\n\n"
191 191 )
192 192 )
193 193 cmd = Command(b'commit --interactive')
194 194 else:
195 195 cmd = Command(b"add")
196 196
197 197 if not opts.get(b'all'):
198 198 cmd.extend(args)
199 199 else:
200 200 ui.status(
201 201 _(
202 202 b"note: use hg addremove to remove files that have "
203 203 b"been deleted\n\n"
204 204 )
205 205 )
206 206
207 207 ui.status((bytes(cmd)), b"\n")
208 208
209 209
210 210 def am(ui, repo, *args, **kwargs):
211 211 cmdoptions = []
212 212 args, opts = parseoptions(ui, cmdoptions, args)
213 213 cmd = Command(b'import')
214 214 ui.status(bytes(cmd), b"\n")
215 215
216 216
217 217 def apply(ui, repo, *args, **kwargs):
218 218 cmdoptions = [
219 219 (b'p', b'p', int, b''),
220 220 (b'', b'directory', b'', b''),
221 221 ]
222 222 args, opts = parseoptions(ui, cmdoptions, args)
223 223
224 224 cmd = Command(b'import --no-commit')
225 225 if opts.get(b'p'):
226 226 cmd[b'-p'] = opts.get(b'p')
227 227 if opts.get(b'directory'):
228 228 cmd[b'--prefix'] = opts.get(b'directory')
229 229 cmd.extend(args)
230 230
231 231 ui.status((bytes(cmd)), b"\n")
232 232
233 233
234 234 def bisect(ui, repo, *args, **kwargs):
235 235 ui.status(_(b"see 'hg help bisect' for how to use bisect\n\n"))
236 236
237 237
238 238 def blame(ui, repo, *args, **kwargs):
239 239 cmdoptions = []
240 240 args, opts = parseoptions(ui, cmdoptions, args)
241 241 cmd = Command(b'annotate -udl')
242 242 cmd.extend([convert(v) for v in args])
243 243 ui.status((bytes(cmd)), b"\n")
244 244
245 245
246 246 def branch(ui, repo, *args, **kwargs):
247 247 cmdoptions = [
248 248 (b'', b'set-upstream', None, b''),
249 249 (b'', b'set-upstream-to', b'', b''),
250 250 (b'd', b'delete', None, b''),
251 251 (b'D', b'delete', None, b''),
252 252 (b'm', b'move', None, b''),
253 253 (b'M', b'move', None, b''),
254 254 ]
255 255 args, opts = parseoptions(ui, cmdoptions, args)
256 256
257 257 cmd = Command(b"bookmark")
258 258
259 259 if opts.get(b'set_upstream') or opts.get(b'set_upstream_to'):
260 260 ui.status(_(b"Mercurial has no concept of upstream branches\n"))
261 261 return
262 262 elif opts.get(b'delete'):
263 263 cmd = Command(b"strip")
264 264 for branch in args:
265 265 cmd[b'-B'] = branch
266 266 else:
267 267 cmd[b'-B'] = None
268 268 elif opts.get(b'move'):
269 269 if len(args) > 0:
270 270 if len(args) > 1:
271 271 old = args.pop(0)
272 272 else:
273 273 # shell command to output the active bookmark for the active
274 274 # revision
275 275 old = b'`hg log -T"{activebookmark}" -r .`'
276 276 else:
277 277 raise error.Abort(_(b'missing newbranch argument'))
278 278 new = args[0]
279 279 cmd[b'-m'] = old
280 280 cmd.append(new)
281 281 else:
282 282 if len(args) > 1:
283 283 cmd[b'-r'] = args[1]
284 284 cmd.append(args[0])
285 285 elif len(args) == 1:
286 286 cmd.append(args[0])
287 287 ui.status((bytes(cmd)), b"\n")
288 288
289 289
290 290 def ispath(repo, string):
291 291 """
292 292 The first argument to git checkout can either be a revision or a path. Let's
293 293 generally assume it's a revision, unless it's obviously a path. There are
294 294 too many ways to spell revisions in git for us to reasonably catch all of
295 295 them, so let's be conservative.
296 296 """
297 297 if scmutil.isrevsymbol(repo, string):
298 298 # if it's definitely a revision let's not even check if a file of the
299 299 # same name exists.
300 300 return False
301 301
302 302 cwd = repo.getcwd()
303 303 if cwd == b'':
304 304 repopath = string
305 305 else:
306 306 repopath = cwd + b'/' + string
307 307
308 308 exists = repo.wvfs.exists(repopath)
309 309 if exists:
310 310 return True
311 311
312 312 manifest = repo[b'.'].manifest()
313 313
314 314 didexist = (repopath in manifest) or manifest.hasdir(repopath)
315 315
316 316 return didexist
317 317
318 318
319 319 def checkout(ui, repo, *args, **kwargs):
320 320 cmdoptions = [
321 321 (b'b', b'branch', b'', b''),
322 322 (b'B', b'branch', b'', b''),
323 323 (b'f', b'force', None, b''),
324 324 (b'p', b'patch', None, b''),
325 325 ]
326 326 paths = []
327 327 if b'--' in args:
328 328 sepindex = args.index(b'--')
329 329 paths.extend(args[sepindex + 1 :])
330 330 args = args[:sepindex]
331 331
332 332 args, opts = parseoptions(ui, cmdoptions, args)
333 333
334 334 rev = None
335 335 if args and ispath(repo, args[0]):
336 336 paths = args + paths
337 337 elif args:
338 338 rev = args[0]
339 339 paths = args[1:] + paths
340 340
341 341 cmd = Command(b'update')
342 342
343 343 if opts.get(b'force'):
344 344 if paths or rev:
345 345 cmd[b'-C'] = None
346 346
347 347 if opts.get(b'patch'):
348 348 cmd = Command(b'revert')
349 349 cmd[b'-i'] = None
350 350
351 351 if opts.get(b'branch'):
352 352 if len(args) == 0:
353 353 cmd = Command(b'bookmark')
354 354 cmd.append(opts.get(b'branch'))
355 355 else:
356 356 cmd.append(args[0])
357 357 bookcmd = Command(b'bookmark')
358 358 bookcmd.append(opts.get(b'branch'))
359 359 cmd = cmd & bookcmd
360 360 # if there is any path argument supplied, use revert instead of update
361 361 elif len(paths) > 0:
362 362 ui.status(_(b"note: use --no-backup to avoid creating .orig files\n\n"))
363 363 cmd = Command(b'revert')
364 364 if opts.get(b'patch'):
365 365 cmd[b'-i'] = None
366 366 if rev:
367 367 cmd[b'-r'] = rev
368 368 cmd.extend(paths)
369 369 elif rev:
370 370 if opts.get(b'patch'):
371 371 cmd[b'-r'] = rev
372 372 else:
373 373 cmd.append(rev)
374 374 elif opts.get(b'force'):
375 375 cmd = Command(b'revert')
376 376 cmd[b'--all'] = None
377 377 else:
378 378 raise error.Abort(_(b"a commit must be specified"))
379 379
380 380 ui.status((bytes(cmd)), b"\n")
381 381
382 382
383 383 def cherrypick(ui, repo, *args, **kwargs):
384 384 cmdoptions = [
385 385 (b'', b'continue', None, b''),
386 386 (b'', b'abort', None, b''),
387 387 (b'e', b'edit', None, b''),
388 388 ]
389 389 args, opts = parseoptions(ui, cmdoptions, args)
390 390
391 391 cmd = Command(b'graft')
392 392
393 393 if opts.get(b'edit'):
394 394 cmd[b'--edit'] = None
395 395 if opts.get(b'continue'):
396 396 cmd[b'--continue'] = None
397 397 elif opts.get(b'abort'):
398 398 ui.status(_(b"note: hg graft does not have --abort\n\n"))
399 399 return
400 400 else:
401 401 cmd.extend(args)
402 402
403 403 ui.status((bytes(cmd)), b"\n")
404 404
405 405
406 406 def clean(ui, repo, *args, **kwargs):
407 407 cmdoptions = [
408 408 (b'd', b'd', None, b''),
409 409 (b'f', b'force', None, b''),
410 410 (b'x', b'x', None, b''),
411 411 ]
412 412 args, opts = parseoptions(ui, cmdoptions, args)
413 413
414 414 cmd = Command(b'purge')
415 415 if opts.get(b'x'):
416 416 cmd[b'--all'] = None
417 417 cmd.extend(args)
418 418
419 419 ui.status((bytes(cmd)), b"\n")
420 420
421 421
422 422 def clone(ui, repo, *args, **kwargs):
423 423 cmdoptions = [
424 424 (b'', b'bare', None, b''),
425 425 (b'n', b'no-checkout', None, b''),
426 426 (b'b', b'branch', b'', b''),
427 427 ]
428 428 args, opts = parseoptions(ui, cmdoptions, args)
429 429
430 430 if len(args) == 0:
431 431 raise error.Abort(_(b"a repository to clone must be specified"))
432 432
433 433 cmd = Command(b'clone')
434 434 cmd.append(args[0])
435 435 if len(args) > 1:
436 436 cmd.append(args[1])
437 437
438 438 if opts.get(b'bare'):
439 439 cmd[b'-U'] = None
440 440 ui.status(
441 441 _(
442 442 b"note: Mercurial does not have bare clones. "
443 443 b"-U will clone the repo without checking out a commit\n\n"
444 444 )
445 445 )
446 446 elif opts.get(b'no_checkout'):
447 447 cmd[b'-U'] = None
448 448
449 449 if opts.get(b'branch'):
450 450 cocmd = Command(b"update")
451 451 cocmd.append(opts.get(b'branch'))
452 452 cmd = cmd & cocmd
453 453
454 454 ui.status((bytes(cmd)), b"\n")
455 455
456 456
457 457 def commit(ui, repo, *args, **kwargs):
458 458 cmdoptions = [
459 459 (b'a', b'all', None, b''),
460 460 (b'm', b'message', b'', b''),
461 461 (b'p', b'patch', None, b''),
462 462 (b'C', b'reuse-message', b'', b''),
463 463 (b'F', b'file', b'', b''),
464 464 (b'', b'author', b'', b''),
465 465 (b'', b'date', b'', b''),
466 466 (b'', b'amend', None, b''),
467 467 (b'', b'no-edit', None, b''),
468 468 ]
469 469 args, opts = parseoptions(ui, cmdoptions, args)
470 470
471 471 cmd = Command(b'commit')
472 472 if opts.get(b'patch'):
473 473 cmd = Command(b'commit --interactive')
474 474
475 475 if opts.get(b'amend'):
476 476 if opts.get(b'no_edit'):
477 477 cmd = Command(b'amend')
478 478 else:
479 479 cmd[b'--amend'] = None
480 480
481 481 if opts.get(b'reuse_message'):
482 482 cmd[b'-M'] = opts.get(b'reuse_message')
483 483
484 484 if opts.get(b'message'):
485 485 cmd[b'-m'] = b"'%s'" % (opts.get(b'message'),)
486 486
487 487 if opts.get(b'all'):
488 488 ui.status(
489 489 _(
490 490 b"note: Mercurial doesn't have a staging area, "
491 491 b"so there is no --all. -A will add and remove files "
492 492 b"for you though.\n\n"
493 493 )
494 494 )
495 495
496 496 if opts.get(b'file'):
497 497 cmd[b'-l'] = opts.get(b'file')
498 498
499 499 if opts.get(b'author'):
500 500 cmd[b'-u'] = opts.get(b'author')
501 501
502 502 if opts.get(b'date'):
503 503 cmd[b'-d'] = opts.get(b'date')
504 504
505 505 cmd.extend(args)
506 506
507 507 ui.status((bytes(cmd)), b"\n")
508 508
509 509
510 510 def deprecated(ui, repo, *args, **kwargs):
511 511 ui.warn(
512 512 _(
513 513 b'this command has been deprecated in the git project, '
514 514 b'thus isn\'t supported by this tool\n\n'
515 515 )
516 516 )
517 517
518 518
519 519 def diff(ui, repo, *args, **kwargs):
520 520 cmdoptions = [
521 521 (b'a', b'all', None, b''),
522 522 (b'', b'cached', None, b''),
523 523 (b'R', b'reverse', None, b''),
524 524 ]
525 525 args, opts = parseoptions(ui, cmdoptions, args)
526 526
527 527 cmd = Command(b'diff')
528 528
529 529 if opts.get(b'cached'):
530 530 ui.status(
531 531 _(
532 532 b'note: Mercurial has no concept of a staging area, '
533 533 b'so --cached does nothing\n\n'
534 534 )
535 535 )
536 536
537 537 if opts.get(b'reverse'):
538 538 cmd[b'--reverse'] = None
539 539
540 540 for a in list(args):
541 541 args.remove(a)
542 542 try:
543 543 repo.revs(a)
544 544 cmd[b'-r'] = a
545 545 except Exception:
546 546 cmd.append(a)
547 547
548 548 ui.status((bytes(cmd)), b"\n")
549 549
550 550
551 551 def difftool(ui, repo, *args, **kwargs):
552 552 ui.status(
553 553 _(
554 554 b'Mercurial does not enable external difftool by default. You '
555 555 b'need to enable the extdiff extension in your .hgrc file by adding\n'
556 556 b'extdiff =\n'
557 557 b'to the [extensions] section and then running\n\n'
558 558 b'hg extdiff -p <program>\n\n'
559 559 b'See \'hg help extdiff\' and \'hg help -e extdiff\' for more '
560 560 b'information.\n'
561 561 )
562 562 )
563 563
564 564
565 565 def fetch(ui, repo, *args, **kwargs):
566 566 cmdoptions = [
567 567 (b'', b'all', None, b''),
568 568 (b'f', b'force', None, b''),
569 569 ]
570 570 args, opts = parseoptions(ui, cmdoptions, args)
571 571
572 572 cmd = Command(b'pull')
573 573
574 574 if len(args) > 0:
575 575 cmd.append(args[0])
576 576 if len(args) > 1:
577 577 ui.status(
578 578 _(
579 579 b"note: Mercurial doesn't have refspecs. "
580 580 b"-r can be used to specify which commits you want to "
581 581 b"pull. -B can be used to specify which bookmark you "
582 582 b"want to pull.\n\n"
583 583 )
584 584 )
585 585 for v in args[1:]:
586 586 if v in repo._bookmarks:
587 587 cmd[b'-B'] = v
588 588 else:
589 589 cmd[b'-r'] = v
590 590
591 591 ui.status((bytes(cmd)), b"\n")
592 592
593 593
594 594 def grep(ui, repo, *args, **kwargs):
595 595 cmdoptions = []
596 596 args, opts = parseoptions(ui, cmdoptions, args)
597 597
598 598 cmd = Command(b'grep')
599 599
600 600 # For basic usage, git grep and hg grep are the same. They both have the
601 601 # pattern first, followed by paths.
602 602 cmd.extend(args)
603 603
604 604 ui.status((bytes(cmd)), b"\n")
605 605
606 606
607 607 def init(ui, repo, *args, **kwargs):
608 608 cmdoptions = []
609 609 args, opts = parseoptions(ui, cmdoptions, args)
610 610
611 611 cmd = Command(b'init')
612 612
613 613 if len(args) > 0:
614 614 cmd.append(args[0])
615 615
616 616 ui.status((bytes(cmd)), b"\n")
617 617
618 618
619 619 def log(ui, repo, *args, **kwargs):
620 620 cmdoptions = [
621 621 (b'', b'follow', None, b''),
622 622 (b'', b'decorate', None, b''),
623 623 (b'n', b'number', b'', b''),
624 624 (b'1', b'1', None, b''),
625 625 (b'', b'pretty', b'', b''),
626 626 (b'', b'format', b'', b''),
627 627 (b'', b'oneline', None, b''),
628 628 (b'', b'stat', None, b''),
629 629 (b'', b'graph', None, b''),
630 630 (b'p', b'patch', None, b''),
631 631 ]
632 632 args, opts = parseoptions(ui, cmdoptions, args)
633 633 ui.status(
634 634 _(
635 635 b'note: -v prints the entire commit message like Git does. To '
636 636 b'print just the first line, drop the -v.\n\n'
637 637 )
638 638 )
639 639 ui.status(
640 640 _(
641 641 b"note: see hg help revset for information on how to filter "
642 642 b"log output\n\n"
643 643 )
644 644 )
645 645
646 646 cmd = Command(b'log')
647 647 cmd[b'-v'] = None
648 648
649 649 if opts.get(b'number'):
650 650 cmd[b'-l'] = opts.get(b'number')
651 651 if opts.get(b'1'):
652 652 cmd[b'-l'] = b'1'
653 653 if opts.get(b'stat'):
654 654 cmd[b'--stat'] = None
655 655 if opts.get(b'graph'):
656 656 cmd[b'-G'] = None
657 657 if opts.get(b'patch'):
658 658 cmd[b'-p'] = None
659 659
660 660 if opts.get(b'pretty') or opts.get(b'format') or opts.get(b'oneline'):
661 661 format = opts.get(b'format', b'')
662 662 if b'format:' in format:
663 663 ui.status(
664 664 _(
665 665 b"note: --format format:??? equates to Mercurial's "
666 666 b"--template. See hg help templates for more info.\n\n"
667 667 )
668 668 )
669 669 cmd[b'--template'] = b'???'
670 670 else:
671 671 ui.status(
672 672 _(
673 673 b"note: --pretty/format/oneline equate to Mercurial's "
674 674 b"--style or --template. See hg help templates for "
675 675 b"more info.\n\n"
676 676 )
677 677 )
678 678 cmd[b'--style'] = b'???'
679 679
680 680 if len(args) > 0:
681 681 if b'..' in args[0]:
682 682 since, until = args[0].split(b'..')
683 683 cmd[b'-r'] = b"'%s::%s'" % (since, until)
684 684 del args[0]
685 685 cmd.extend(args)
686 686
687 687 ui.status((bytes(cmd)), b"\n")
688 688
689 689
690 690 def lsfiles(ui, repo, *args, **kwargs):
691 691 cmdoptions = [
692 692 (b'c', b'cached', None, b''),
693 693 (b'd', b'deleted', None, b''),
694 694 (b'm', b'modified', None, b''),
695 695 (b'o', b'others', None, b''),
696 696 (b'i', b'ignored', None, b''),
697 697 (b's', b'stage', None, b''),
698 698 (b'z', b'_zero', None, b''),
699 699 ]
700 700 args, opts = parseoptions(ui, cmdoptions, args)
701 701
702 702 if (
703 703 opts.get(b'modified')
704 704 or opts.get(b'deleted')
705 705 or opts.get(b'others')
706 706 or opts.get(b'ignored')
707 707 ):
708 708 cmd = Command(b'status')
709 709 if opts.get(b'deleted'):
710 710 cmd[b'-d'] = None
711 711 if opts.get(b'modified'):
712 712 cmd[b'-m'] = None
713 713 if opts.get(b'others'):
714 714 cmd[b'-o'] = None
715 715 if opts.get(b'ignored'):
716 716 cmd[b'-i'] = None
717 717 else:
718 718 cmd = Command(b'files')
719 719 if opts.get(b'stage'):
720 720 ui.status(
721 721 _(
722 722 b"note: Mercurial doesn't have a staging area, ignoring "
723 723 b"--stage\n"
724 724 )
725 725 )
726 726 if opts.get(b'_zero'):
727 727 cmd[b'-0'] = None
728 728 cmd.append(b'.')
729 729 for include in args:
730 730 cmd[b'-I'] = procutil.shellquote(include)
731 731
732 732 ui.status((bytes(cmd)), b"\n")
733 733
734 734
735 735 def merge(ui, repo, *args, **kwargs):
736 736 cmdoptions = []
737 737 args, opts = parseoptions(ui, cmdoptions, args)
738 738
739 739 cmd = Command(b'merge')
740 740
741 741 if len(args) > 0:
742 742 cmd.append(args[len(args) - 1])
743 743
744 744 ui.status((bytes(cmd)), b"\n")
745 745
746 746
747 747 def mergebase(ui, repo, *args, **kwargs):
748 748 cmdoptions = []
749 749 args, opts = parseoptions(ui, cmdoptions, args)
750 750
751 751 if len(args) != 2:
752 752 args = [b'A', b'B']
753 753
754 754 cmd = Command(
755 755 b"log -T '{node}\\n' -r 'ancestor(%s,%s)'" % (args[0], args[1])
756 756 )
757 757
758 758 ui.status(
759 759 _(b'note: ancestors() is part of the revset language\n'),
760 760 _(b"(learn more about revsets with 'hg help revsets')\n\n"),
761 761 )
762 762 ui.status((bytes(cmd)), b"\n")
763 763
764 764
765 765 def mergetool(ui, repo, *args, **kwargs):
766 766 cmdoptions = []
767 767 args, opts = parseoptions(ui, cmdoptions, args)
768 768
769 769 cmd = Command(b"resolve")
770 770
771 771 if len(args) == 0:
772 772 cmd[b'--all'] = None
773 773 cmd.extend(args)
774 774 ui.status((bytes(cmd)), b"\n")
775 775
776 776
777 777 def mv(ui, repo, *args, **kwargs):
778 778 cmdoptions = [
779 779 (b'f', b'force', None, b''),
780 780 (b'n', b'dry-run', None, b''),
781 781 ]
782 782 args, opts = parseoptions(ui, cmdoptions, args)
783 783
784 784 cmd = Command(b'mv')
785 785 cmd.extend(args)
786 786
787 787 if opts.get(b'force'):
788 788 cmd[b'-f'] = None
789 789 if opts.get(b'dry_run'):
790 790 cmd[b'-n'] = None
791 791
792 792 ui.status((bytes(cmd)), b"\n")
793 793
794 794
795 795 def pull(ui, repo, *args, **kwargs):
796 796 cmdoptions = [
797 797 (b'', b'all', None, b''),
798 798 (b'f', b'force', None, b''),
799 799 (b'r', b'rebase', None, b''),
800 800 ]
801 801 args, opts = parseoptions(ui, cmdoptions, args)
802 802
803 803 cmd = Command(b'pull')
804 804 cmd[b'--rebase'] = None
805 805
806 806 if len(args) > 0:
807 807 cmd.append(args[0])
808 808 if len(args) > 1:
809 809 ui.status(
810 810 _(
811 811 b"note: Mercurial doesn't have refspecs. "
812 812 b"-r can be used to specify which commits you want to "
813 813 b"pull. -B can be used to specify which bookmark you "
814 814 b"want to pull.\n\n"
815 815 )
816 816 )
817 817 for v in args[1:]:
818 818 if v in repo._bookmarks:
819 819 cmd[b'-B'] = v
820 820 else:
821 821 cmd[b'-r'] = v
822 822
823 823 ui.status((bytes(cmd)), b"\n")
824 824
825 825
826 826 def push(ui, repo, *args, **kwargs):
827 827 cmdoptions = [
828 828 (b'', b'all', None, b''),
829 829 (b'f', b'force', None, b''),
830 830 ]
831 831 args, opts = parseoptions(ui, cmdoptions, args)
832 832
833 833 cmd = Command(b'push')
834 834
835 835 if len(args) > 0:
836 836 cmd.append(args[0])
837 837 if len(args) > 1:
838 838 ui.status(
839 839 _(
840 840 b"note: Mercurial doesn't have refspecs. "
841 841 b"-r can be used to specify which commits you want "
842 842 b"to push. -B can be used to specify which bookmark "
843 843 b"you want to push.\n\n"
844 844 )
845 845 )
846 846 for v in args[1:]:
847 847 if v in repo._bookmarks:
848 848 cmd[b'-B'] = v
849 849 else:
850 850 cmd[b'-r'] = v
851 851
852 852 if opts.get(b'force'):
853 853 cmd[b'-f'] = None
854 854
855 855 ui.status((bytes(cmd)), b"\n")
856 856
857 857
858 858 def rebase(ui, repo, *args, **kwargs):
859 859 cmdoptions = [
860 860 (b'', b'all', None, b''),
861 861 (b'i', b'interactive', None, b''),
862 862 (b'', b'onto', b'', b''),
863 863 (b'', b'abort', None, b''),
864 864 (b'', b'continue', None, b''),
865 865 (b'', b'skip', None, b''),
866 866 ]
867 867 args, opts = parseoptions(ui, cmdoptions, args)
868 868
869 869 if opts.get(b'interactive'):
870 870 ui.status(
871 871 _(
872 872 b"note: hg histedit does not perform a rebase. "
873 873 b"It just edits history.\n\n"
874 874 )
875 875 )
876 876 cmd = Command(b'histedit')
877 877 if len(args) > 0:
878 878 ui.status(
879 879 _(
880 880 b"also note: 'hg histedit' will automatically detect"
881 881 b" your stack, so no second argument is necessary\n\n"
882 882 )
883 883 )
884 884 ui.status((bytes(cmd)), b"\n")
885 885 return
886 886
887 887 if opts.get(b'skip'):
888 888 cmd = Command(b'revert --all -r .')
889 889 ui.status((bytes(cmd)), b"\n")
890 890
891 891 cmd = Command(b'rebase')
892 892
893 893 if opts.get(b'continue') or opts.get(b'skip'):
894 894 cmd[b'--continue'] = None
895 895 if opts.get(b'abort'):
896 896 cmd[b'--abort'] = None
897 897
898 898 if opts.get(b'onto'):
899 899 ui.status(
900 900 _(
901 901 b"note: if you're trying to lift a commit off one branch, "
902 902 b"try hg rebase -d <destination commit> -s <commit to be "
903 903 b"lifted>\n\n"
904 904 )
905 905 )
906 906 cmd[b'-d'] = convert(opts.get(b'onto'))
907 907 if len(args) < 2:
908 908 raise error.Abort(_(b"expected format: git rebase --onto X Y Z"))
909 909 cmd[b'-s'] = b"'::%s - ::%s'" % (convert(args[1]), convert(args[0]))
910 910 else:
911 911 if len(args) == 1:
912 912 cmd[b'-d'] = convert(args[0])
913 913 elif len(args) == 2:
914 914 cmd[b'-d'] = convert(args[0])
915 915 cmd[b'-b'] = convert(args[1])
916 916
917 917 ui.status((bytes(cmd)), b"\n")
918 918
919 919
920 920 def reflog(ui, repo, *args, **kwargs):
921 921 cmdoptions = [
922 922 (b'', b'all', None, b''),
923 923 ]
924 924 args, opts = parseoptions(ui, cmdoptions, args)
925 925
926 926 cmd = Command(b'journal')
927 927 if opts.get(b'all'):
928 928 cmd[b'--all'] = None
929 929 if len(args) > 0:
930 930 cmd.append(args[0])
931 931
932 932 ui.status(bytes(cmd), b"\n\n")
933 933 ui.status(
934 934 _(
935 935 b"note: in hg commits can be deleted from repo but we always"
936 936 b" have backups\n"
937 937 )
938 938 )
939 939
940 940
941 941 def reset(ui, repo, *args, **kwargs):
942 942 cmdoptions = [
943 943 (b'', b'soft', None, b''),
944 944 (b'', b'hard', None, b''),
945 945 (b'', b'mixed', None, b''),
946 946 ]
947 947 args, opts = parseoptions(ui, cmdoptions, args)
948 948
949 949 commit = convert(args[0] if len(args) > 0 else b'.')
950 950 hard = opts.get(b'hard')
951 951
952 952 if opts.get(b'mixed'):
953 953 ui.status(
954 954 _(
955 955 b'note: --mixed has no meaning since Mercurial has no '
956 956 b'staging area\n\n'
957 957 )
958 958 )
959 959 if opts.get(b'soft'):
960 960 ui.status(
961 961 _(
962 962 b'note: --soft has no meaning since Mercurial has no '
963 963 b'staging area\n\n'
964 964 )
965 965 )
966 966
967 967 cmd = Command(b'update')
968 968 if hard:
969 969 cmd.append(b'--clean')
970 970
971 971 cmd.append(commit)
972 972
973 973 ui.status((bytes(cmd)), b"\n")
974 974
975 975
976 976 def revert(ui, repo, *args, **kwargs):
977 977 cmdoptions = []
978 978 args, opts = parseoptions(ui, cmdoptions, args)
979 979
980 980 if len(args) > 1:
981 981 ui.status(
982 982 _(
983 983 b"note: hg backout doesn't support multiple commits at "
984 984 b"once\n\n"
985 985 )
986 986 )
987 987
988 988 cmd = Command(b'backout')
989 989 if args:
990 990 cmd.append(args[0])
991 991
992 992 ui.status((bytes(cmd)), b"\n")
993 993
994 994
995 995 def revparse(ui, repo, *args, **kwargs):
996 996 cmdoptions = [
997 997 (b'', b'show-cdup', None, b''),
998 998 (b'', b'show-toplevel', None, b''),
999 999 ]
1000 1000 args, opts = parseoptions(ui, cmdoptions, args)
1001 1001
1002 1002 if opts.get(b'show_cdup') or opts.get(b'show_toplevel'):
1003 1003 cmd = Command(b'root')
1004 1004 if opts.get(b'show_cdup'):
1005 1005 ui.status(_(b"note: hg root prints the root of the repository\n\n"))
1006 1006 ui.status((bytes(cmd)), b"\n")
1007 1007 else:
1008 1008 ui.status(_(b"note: see hg help revset for how to refer to commits\n"))
1009 1009
1010 1010
1011 1011 def rm(ui, repo, *args, **kwargs):
1012 1012 cmdoptions = [
1013 1013 (b'f', b'force', None, b''),
1014 1014 (b'n', b'dry-run', None, b''),
1015 1015 ]
1016 1016 args, opts = parseoptions(ui, cmdoptions, args)
1017 1017
1018 1018 cmd = Command(b'rm')
1019 1019 cmd.extend(args)
1020 1020
1021 1021 if opts.get(b'force'):
1022 1022 cmd[b'-f'] = None
1023 1023 if opts.get(b'dry_run'):
1024 1024 cmd[b'-n'] = None
1025 1025
1026 1026 ui.status((bytes(cmd)), b"\n")
1027 1027
1028 1028
1029 1029 def show(ui, repo, *args, **kwargs):
1030 1030 cmdoptions = [
1031 1031 (b'', b'name-status', None, b''),
1032 1032 (b'', b'pretty', b'', b''),
1033 1033 (b'U', b'unified', int, b''),
1034 1034 ]
1035 1035 args, opts = parseoptions(ui, cmdoptions, args)
1036 1036
1037 1037 if opts.get(b'name_status'):
1038 1038 if opts.get(b'pretty') == b'format:':
1039 1039 cmd = Command(b'status')
1040 1040 cmd[b'--change'] = b'.'
1041 1041 else:
1042 1042 cmd = Command(b'log')
1043 1043 cmd.append(b'--style status')
1044 1044 cmd.append(b'-r .')
1045 1045 elif len(args) > 0:
1046 1046 if ispath(repo, args[0]):
1047 1047 cmd = Command(b'cat')
1048 1048 else:
1049 1049 cmd = Command(b'export')
1050 1050 cmd.extend(args)
1051 1051 if opts.get(b'unified'):
1052 1052 cmd.append(b'--config diff.unified=%d' % (opts[b'unified'],))
1053 1053 elif opts.get(b'unified'):
1054 1054 cmd = Command(b'export')
1055 1055 cmd.append(b'--config diff.unified=%d' % (opts[b'unified'],))
1056 1056 else:
1057 1057 cmd = Command(b'export')
1058 1058
1059 1059 ui.status((bytes(cmd)), b"\n")
1060 1060
1061 1061
1062 1062 def stash(ui, repo, *args, **kwargs):
1063 1063 cmdoptions = [
1064 1064 (b'p', b'patch', None, b''),
1065 1065 ]
1066 1066 args, opts = parseoptions(ui, cmdoptions, args)
1067 1067
1068 1068 cmd = Command(b'shelve')
1069 1069 action = args[0] if len(args) > 0 else None
1070 1070
1071 1071 if action == b'list':
1072 1072 cmd[b'-l'] = None
1073 1073 if opts.get(b'patch'):
1074 1074 cmd[b'-p'] = None
1075 1075 elif action == b'show':
1076 1076 if opts.get(b'patch'):
1077 1077 cmd[b'-p'] = None
1078 1078 else:
1079 1079 cmd[b'--stat'] = None
1080 1080 if len(args) > 1:
1081 1081 cmd.append(args[1])
1082 1082 elif action == b'clear':
1083 1083 cmd[b'--cleanup'] = None
1084 1084 elif action == b'drop':
1085 1085 cmd[b'-d'] = None
1086 1086 if len(args) > 1:
1087 1087 cmd.append(args[1])
1088 1088 else:
1089 1089 cmd.append(b'<shelve name>')
1090 1090 elif action == b'pop' or action == b'apply':
1091 1091 cmd = Command(b'unshelve')
1092 1092 if len(args) > 1:
1093 1093 cmd.append(args[1])
1094 1094 if action == b'apply':
1095 1095 cmd[b'--keep'] = None
1096 1096 elif action == b'branch' or action == b'create':
1097 1097 ui.status(
1098 1098 _(
1099 1099 b"note: Mercurial doesn't have equivalents to the "
1100 1100 b"git stash branch or create actions\n\n"
1101 1101 )
1102 1102 )
1103 1103 return
1104 1104 else:
1105 1105 if len(args) > 0:
1106 1106 if args[0] != b'save':
1107 1107 cmd[b'--name'] = args[0]
1108 1108 elif len(args) > 1:
1109 1109 cmd[b'--name'] = args[1]
1110 1110
1111 1111 ui.status((bytes(cmd)), b"\n")
1112 1112
1113 1113
1114 1114 def status(ui, repo, *args, **kwargs):
1115 1115 cmdoptions = [
1116 1116 (b'', b'ignored', None, b''),
1117 1117 ]
1118 1118 args, opts = parseoptions(ui, cmdoptions, args)
1119 1119
1120 1120 cmd = Command(b'status')
1121 1121 cmd.extend(args)
1122 1122
1123 1123 if opts.get(b'ignored'):
1124 1124 cmd[b'-i'] = None
1125 1125
1126 1126 ui.status((bytes(cmd)), b"\n")
1127 1127
1128 1128
1129 1129 def svn(ui, repo, *args, **kwargs):
1130 1130 if not args:
1131 1131 raise error.Abort(_(b'missing svn command'))
1132 1132 svncmd = args[0]
1133 1133 if svncmd not in gitsvncommands:
1134 1134 raise error.Abort(_(b'unknown git svn command "%s"') % svncmd)
1135 1135
1136 1136 args = args[1:]
1137 1137 return gitsvncommands[svncmd](ui, repo, *args, **kwargs)
1138 1138
1139 1139
1140 1140 def svndcommit(ui, repo, *args, **kwargs):
1141 1141 cmdoptions = []
1142 1142 args, opts = parseoptions(ui, cmdoptions, args)
1143 1143
1144 1144 cmd = Command(b'push')
1145 1145
1146 1146 ui.status((bytes(cmd)), b"\n")
1147 1147
1148 1148
1149 1149 def svnfetch(ui, repo, *args, **kwargs):
1150 1150 cmdoptions = []
1151 1151 args, opts = parseoptions(ui, cmdoptions, args)
1152 1152
1153 1153 cmd = Command(b'pull')
1154 1154 cmd.append(b'default-push')
1155 1155
1156 1156 ui.status((bytes(cmd)), b"\n")
1157 1157
1158 1158
1159 1159 def svnfindrev(ui, repo, *args, **kwargs):
1160 1160 cmdoptions = []
1161 1161 args, opts = parseoptions(ui, cmdoptions, args)
1162 1162
1163 1163 if not args:
1164 1164 raise error.Abort(_(b'missing find-rev argument'))
1165 1165
1166 1166 cmd = Command(b'log')
1167 1167 cmd[b'-r'] = args[0]
1168 1168
1169 1169 ui.status((bytes(cmd)), b"\n")
1170 1170
1171 1171
1172 1172 def svnrebase(ui, repo, *args, **kwargs):
1173 1173 cmdoptions = [
1174 1174 (b'l', b'local', None, b''),
1175 1175 ]
1176 1176 args, opts = parseoptions(ui, cmdoptions, args)
1177 1177
1178 1178 pullcmd = Command(b'pull')
1179 1179 pullcmd.append(b'default-push')
1180 1180 rebasecmd = Command(b'rebase')
1181 1181 rebasecmd.append(b'tip')
1182 1182
1183 1183 cmd = pullcmd & rebasecmd
1184 1184
1185 1185 ui.status((bytes(cmd)), b"\n")
1186 1186
1187 1187
1188 1188 def tag(ui, repo, *args, **kwargs):
1189 1189 cmdoptions = [
1190 1190 (b'f', b'force', None, b''),
1191 1191 (b'l', b'list', None, b''),
1192 1192 (b'd', b'delete', None, b''),
1193 1193 ]
1194 1194 args, opts = parseoptions(ui, cmdoptions, args)
1195 1195
1196 1196 if opts.get(b'list'):
1197 1197 cmd = Command(b'tags')
1198 1198 else:
1199 1199 cmd = Command(b'tag')
1200 1200
1201 1201 if not args:
1202 1202 raise error.Abort(_(b'missing tag argument'))
1203 1203
1204 1204 cmd.append(args[0])
1205 1205 if len(args) > 1:
1206 1206 cmd[b'-r'] = args[1]
1207 1207
1208 1208 if opts.get(b'delete'):
1209 1209 cmd[b'--remove'] = None
1210 1210
1211 1211 if opts.get(b'force'):
1212 1212 cmd[b'-f'] = None
1213 1213
1214 1214 ui.status((bytes(cmd)), b"\n")
1215 1215
1216 1216
1217 1217 gitcommands = {
1218 1218 b'add': add,
1219 1219 b'am': am,
1220 1220 b'apply': apply,
1221 1221 b'bisect': bisect,
1222 1222 b'blame': blame,
1223 1223 b'branch': branch,
1224 1224 b'checkout': checkout,
1225 1225 b'cherry-pick': cherrypick,
1226 1226 b'clean': clean,
1227 1227 b'clone': clone,
1228 1228 b'commit': commit,
1229 1229 b'diff': diff,
1230 1230 b'difftool': difftool,
1231 1231 b'fetch': fetch,
1232 1232 b'grep': grep,
1233 1233 b'init': init,
1234 1234 b'log': log,
1235 1235 b'ls-files': lsfiles,
1236 1236 b'merge': merge,
1237 1237 b'merge-base': mergebase,
1238 1238 b'mergetool': mergetool,
1239 1239 b'mv': mv,
1240 1240 b'pull': pull,
1241 1241 b'push': push,
1242 1242 b'rebase': rebase,
1243 1243 b'reflog': reflog,
1244 1244 b'reset': reset,
1245 1245 b'revert': revert,
1246 1246 b'rev-parse': revparse,
1247 1247 b'rm': rm,
1248 1248 b'show': show,
1249 1249 b'stash': stash,
1250 1250 b'status': status,
1251 1251 b'svn': svn,
1252 1252 b'tag': tag,
1253 1253 b'whatchanged': deprecated,
1254 1254 }
1255 1255
1256 1256 gitsvncommands = {
1257 1257 b'dcommit': svndcommit,
1258 1258 b'fetch': svnfetch,
1259 1259 b'find-rev': svnfindrev,
1260 1260 b'rebase': svnrebase,
1261 1261 }
@@ -1,2650 +1,2650 b''
1 1 # histedit.py - interactive history editing for mercurial
2 2 #
3 3 # Copyright 2009 Augie Fackler <raf@durin42.com>
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 """interactive history editing
8 8
9 9 With this extension installed, Mercurial gains one new command: histedit. Usage
10 10 is as follows, assuming the following history::
11 11
12 12 @ 3[tip] 7c2fd3b9020c 2009-04-27 18:04 -0500 durin42
13 13 | Add delta
14 14 |
15 15 o 2 030b686bedc4 2009-04-27 18:04 -0500 durin42
16 16 | Add gamma
17 17 |
18 18 o 1 c561b4e977df 2009-04-27 18:04 -0500 durin42
19 19 | Add beta
20 20 |
21 21 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
22 22 Add alpha
23 23
24 24 If you were to run ``hg histedit c561b4e977df``, you would see the following
25 25 file open in your editor::
26 26
27 27 pick c561b4e977df Add beta
28 28 pick 030b686bedc4 Add gamma
29 29 pick 7c2fd3b9020c Add delta
30 30
31 31 # Edit history between c561b4e977df and 7c2fd3b9020c
32 32 #
33 33 # Commits are listed from least to most recent
34 34 #
35 35 # Commands:
36 36 # p, pick = use commit
37 37 # e, edit = use commit, but stop for amending
38 38 # f, fold = use commit, but combine it with the one above
39 39 # r, roll = like fold, but discard this commit's description and date
40 40 # d, drop = remove commit from history
41 41 # m, mess = edit commit message without changing commit content
42 42 # b, base = checkout changeset and apply further changesets from there
43 43 #
44 44
45 45 In this file, lines beginning with ``#`` are ignored. You must specify a rule
46 46 for each revision in your history. For example, if you had meant to add gamma
47 47 before beta, and then wanted to add delta in the same revision as beta, you
48 48 would reorganize the file to look like this::
49 49
50 50 pick 030b686bedc4 Add gamma
51 51 pick c561b4e977df Add beta
52 52 fold 7c2fd3b9020c Add delta
53 53
54 54 # Edit history between c561b4e977df and 7c2fd3b9020c
55 55 #
56 56 # Commits are listed from least to most recent
57 57 #
58 58 # Commands:
59 59 # p, pick = use commit
60 60 # e, edit = use commit, but stop for amending
61 61 # f, fold = use commit, but combine it with the one above
62 62 # r, roll = like fold, but discard this commit's description and date
63 63 # d, drop = remove commit from history
64 64 # m, mess = edit commit message without changing commit content
65 65 # b, base = checkout changeset and apply further changesets from there
66 66 #
67 67
68 68 At which point you close the editor and ``histedit`` starts working. When you
69 69 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
70 70 those revisions together, offering you a chance to clean up the commit message::
71 71
72 72 Add beta
73 73 ***
74 74 Add delta
75 75
76 76 Edit the commit message to your liking, then close the editor. The date used
77 77 for the commit will be the later of the two commits' dates. For this example,
78 78 let's assume that the commit message was changed to ``Add beta and delta.``
79 79 After histedit has run and had a chance to remove any old or temporary
80 80 revisions it needed, the history looks like this::
81 81
82 82 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
83 83 | Add beta and delta.
84 84 |
85 85 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
86 86 | Add gamma
87 87 |
88 88 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
89 89 Add alpha
90 90
91 91 Note that ``histedit`` does *not* remove any revisions (even its own temporary
92 92 ones) until after it has completed all the editing operations, so it will
93 93 probably perform several strip operations when it's done. For the above example,
94 94 it had to run strip twice. Strip can be slow depending on a variety of factors,
95 95 so you might need to be a little patient. You can choose to keep the original
96 96 revisions by passing the ``--keep`` flag.
97 97
98 98 The ``edit`` operation will drop you back to a command prompt,
99 99 allowing you to edit files freely, or even use ``hg record`` to commit
100 100 some changes as a separate commit. When you're done, any remaining
101 101 uncommitted changes will be committed as well. When done, run ``hg
102 102 histedit --continue`` to finish this step. If there are uncommitted
103 103 changes, you'll be prompted for a new commit message, but the default
104 104 commit message will be the original message for the ``edit`` ed
105 105 revision, and the date of the original commit will be preserved.
106 106
107 107 The ``message`` operation will give you a chance to revise a commit
108 108 message without changing the contents. It's a shortcut for doing
109 109 ``edit`` immediately followed by `hg histedit --continue``.
110 110
111 111 If ``histedit`` encounters a conflict when moving a revision (while
112 112 handling ``pick`` or ``fold``), it'll stop in a similar manner to
113 113 ``edit`` with the difference that it won't prompt you for a commit
114 114 message when done. If you decide at this point that you don't like how
115 115 much work it will be to rearrange history, or that you made a mistake,
116 116 you can use ``hg histedit --abort`` to abandon the new changes you
117 117 have made and return to the state before you attempted to edit your
118 118 history.
119 119
120 120 If we clone the histedit-ed example repository above and add four more
121 121 changes, such that we have the following history::
122 122
123 123 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
124 124 | Add theta
125 125 |
126 126 o 5 140988835471 2009-04-27 18:04 -0500 stefan
127 127 | Add eta
128 128 |
129 129 o 4 122930637314 2009-04-27 18:04 -0500 stefan
130 130 | Add zeta
131 131 |
132 132 o 3 836302820282 2009-04-27 18:04 -0500 stefan
133 133 | Add epsilon
134 134 |
135 135 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
136 136 | Add beta and delta.
137 137 |
138 138 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
139 139 | Add gamma
140 140 |
141 141 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
142 142 Add alpha
143 143
144 144 If you run ``hg histedit --outgoing`` on the clone then it is the same
145 145 as running ``hg histedit 836302820282``. If you need plan to push to a
146 146 repository that Mercurial does not detect to be related to the source
147 147 repo, you can add a ``--force`` option.
148 148
149 149 Config
150 150 ------
151 151
152 152 Histedit rule lines are truncated to 80 characters by default. You
153 153 can customize this behavior by setting a different length in your
154 154 configuration file::
155 155
156 156 [histedit]
157 157 linelen = 120 # truncate rule lines at 120 characters
158 158
159 159 The summary of a change can be customized as well::
160 160
161 161 [histedit]
162 162 summary-template = '{rev} {bookmarks} {desc|firstline}'
163 163
164 164 The customized summary should be kept short enough that rule lines
165 165 will fit in the configured line length. See above if that requires
166 166 customization.
167 167
168 168 ``hg histedit`` attempts to automatically choose an appropriate base
169 169 revision to use. To change which base revision is used, define a
170 170 revset in your configuration file::
171 171
172 172 [histedit]
173 173 defaultrev = only(.) & draft()
174 174
175 175 By default each edited revision needs to be present in histedit commands.
176 176 To remove revision you need to use ``drop`` operation. You can configure
177 177 the drop to be implicit for missing commits by adding::
178 178
179 179 [histedit]
180 180 dropmissing = True
181 181
182 182 By default, histedit will close the transaction after each action. For
183 183 performance purposes, you can configure histedit to use a single transaction
184 184 across the entire histedit. WARNING: This setting introduces a significant risk
185 185 of losing the work you've done in a histedit if the histedit aborts
186 186 unexpectedly::
187 187
188 188 [histedit]
189 189 singletransaction = True
190 190
191 191 """
192 192
193 193 from __future__ import absolute_import
194 194
195 195 # chistedit dependencies that are not available everywhere
196 196 try:
197 197 import fcntl
198 198 import termios
199 199 except ImportError:
200 200 fcntl = None
201 201 termios = None
202 202
203 203 import functools
204 204 import locale
205 205 import os
206 206 import struct
207 207
208 208 from mercurial.i18n import _
209 209 from mercurial.pycompat import (
210 210 getattr,
211 211 open,
212 212 )
213 213 from mercurial import (
214 214 bundle2,
215 215 cmdutil,
216 216 context,
217 217 copies,
218 218 destutil,
219 219 discovery,
220 220 encoding,
221 221 error,
222 222 exchange,
223 223 extensions,
224 224 hg,
225 225 logcmdutil,
226 226 merge as mergemod,
227 227 mergeutil,
228 228 node,
229 229 obsolete,
230 230 pycompat,
231 231 registrar,
232 232 repair,
233 233 scmutil,
234 234 state as statemod,
235 235 util,
236 236 )
237 237 from mercurial.utils import (
238 238 dateutil,
239 239 stringutil,
240 240 )
241 241
242 242 pickle = util.pickle
243 243 cmdtable = {}
244 244 command = registrar.command(cmdtable)
245 245
246 246 configtable = {}
247 247 configitem = registrar.configitem(configtable)
248 248 configitem(
249 249 b'experimental', b'histedit.autoverb', default=False,
250 250 )
251 251 configitem(
252 252 b'histedit', b'defaultrev', default=None,
253 253 )
254 254 configitem(
255 255 b'histedit', b'dropmissing', default=False,
256 256 )
257 257 configitem(
258 258 b'histedit', b'linelen', default=80,
259 259 )
260 260 configitem(
261 261 b'histedit', b'singletransaction', default=False,
262 262 )
263 263 configitem(
264 264 b'ui', b'interface.histedit', default=None,
265 265 )
266 266 configitem(b'histedit', b'summary-template', default=b'{rev} {desc|firstline}')
267 267
268 268 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
269 269 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
270 270 # be specifying the version(s) of Mercurial they are tested with, or
271 271 # leave the attribute unspecified.
272 272 testedwith = b'ships-with-hg-core'
273 273
274 274 actiontable = {}
275 275 primaryactions = set()
276 276 secondaryactions = set()
277 277 tertiaryactions = set()
278 278 internalactions = set()
279 279
280 280
281 281 def geteditcomment(ui, first, last):
282 282 """ construct the editor comment
283 283 The comment includes::
284 284 - an intro
285 285 - sorted primary commands
286 286 - sorted short commands
287 287 - sorted long commands
288 288 - additional hints
289 289
290 290 Commands are only included once.
291 291 """
292 292 intro = _(
293 293 """Edit history between %s and %s
294 294
295 295 Commits are listed from least to most recent
296 296
297 297 You can reorder changesets by reordering the lines
298 298
299 299 Commands:
300 300 """
301 301 )
302 302 actions = []
303 303
304 304 def addverb(v):
305 305 a = actiontable[v]
306 306 lines = a.message.split(b"\n")
307 307 if len(a.verbs):
308 308 v = b', '.join(sorted(a.verbs, key=lambda v: len(v)))
309 309 actions.append(b" %s = %s" % (v, lines[0]))
310 310 actions.extend([b' %s' for l in lines[1:]])
311 311
312 312 for v in (
313 313 sorted(primaryactions)
314 314 + sorted(secondaryactions)
315 315 + sorted(tertiaryactions)
316 316 ):
317 317 addverb(v)
318 318 actions.append(b'')
319 319
320 320 hints = []
321 321 if ui.configbool(b'histedit', b'dropmissing'):
322 322 hints.append(
323 323 b"Deleting a changeset from the list "
324 324 b"will DISCARD it from the edited history!"
325 325 )
326 326
327 327 lines = (intro % (first, last)).split(b'\n') + actions + hints
328 328
329 329 return b''.join([b'# %s\n' % l if l else b'#\n' for l in lines])
330 330
331 331
332 332 class histeditstate(object):
333 333 def __init__(self, repo):
334 334 self.repo = repo
335 335 self.actions = None
336 336 self.keep = None
337 337 self.topmost = None
338 338 self.parentctxnode = None
339 339 self.lock = None
340 340 self.wlock = None
341 341 self.backupfile = None
342 342 self.stateobj = statemod.cmdstate(repo, b'histedit-state')
343 343 self.replacements = []
344 344
345 345 def read(self):
346 346 """Load histedit state from disk and set fields appropriately."""
347 347 if not self.stateobj.exists():
348 348 cmdutil.wrongtooltocontinue(self.repo, _(b'histedit'))
349 349
350 350 data = self._read()
351 351
352 352 self.parentctxnode = data[b'parentctxnode']
353 353 actions = parserules(data[b'rules'], self)
354 354 self.actions = actions
355 355 self.keep = data[b'keep']
356 356 self.topmost = data[b'topmost']
357 357 self.replacements = data[b'replacements']
358 358 self.backupfile = data[b'backupfile']
359 359
360 360 def _read(self):
361 361 fp = self.repo.vfs.read(b'histedit-state')
362 362 if fp.startswith(b'v1\n'):
363 363 data = self._load()
364 364 parentctxnode, rules, keep, topmost, replacements, backupfile = data
365 365 else:
366 366 data = pickle.loads(fp)
367 367 parentctxnode, rules, keep, topmost, replacements = data
368 368 backupfile = None
369 369 rules = b"\n".join([b"%s %s" % (verb, rest) for [verb, rest] in rules])
370 370
371 371 return {
372 372 b'parentctxnode': parentctxnode,
373 373 b"rules": rules,
374 374 b"keep": keep,
375 375 b"topmost": topmost,
376 376 b"replacements": replacements,
377 377 b"backupfile": backupfile,
378 378 }
379 379
380 380 def write(self, tr=None):
381 381 if tr:
382 382 tr.addfilegenerator(
383 383 b'histedit-state',
384 384 (b'histedit-state',),
385 385 self._write,
386 386 location=b'plain',
387 387 )
388 388 else:
389 389 with self.repo.vfs(b"histedit-state", b"w") as f:
390 390 self._write(f)
391 391
392 392 def _write(self, fp):
393 393 fp.write(b'v1\n')
394 394 fp.write(b'%s\n' % node.hex(self.parentctxnode))
395 395 fp.write(b'%s\n' % node.hex(self.topmost))
396 396 fp.write(b'%s\n' % (b'True' if self.keep else b'False'))
397 397 fp.write(b'%d\n' % len(self.actions))
398 398 for action in self.actions:
399 399 fp.write(b'%s\n' % action.tostate())
400 400 fp.write(b'%d\n' % len(self.replacements))
401 401 for replacement in self.replacements:
402 402 fp.write(
403 403 b'%s%s\n'
404 404 % (
405 405 node.hex(replacement[0]),
406 406 b''.join(node.hex(r) for r in replacement[1]),
407 407 )
408 408 )
409 409 backupfile = self.backupfile
410 410 if not backupfile:
411 411 backupfile = b''
412 412 fp.write(b'%s\n' % backupfile)
413 413
414 414 def _load(self):
415 415 fp = self.repo.vfs(b'histedit-state', b'r')
416 416 lines = [l[:-1] for l in fp.readlines()]
417 417
418 418 index = 0
419 419 lines[index] # version number
420 420 index += 1
421 421
422 422 parentctxnode = node.bin(lines[index])
423 423 index += 1
424 424
425 425 topmost = node.bin(lines[index])
426 426 index += 1
427 427
428 428 keep = lines[index] == b'True'
429 429 index += 1
430 430
431 431 # Rules
432 432 rules = []
433 433 rulelen = int(lines[index])
434 434 index += 1
435 435 for i in pycompat.xrange(rulelen):
436 436 ruleaction = lines[index]
437 437 index += 1
438 438 rule = lines[index]
439 439 index += 1
440 440 rules.append((ruleaction, rule))
441 441
442 442 # Replacements
443 443 replacements = []
444 444 replacementlen = int(lines[index])
445 445 index += 1
446 446 for i in pycompat.xrange(replacementlen):
447 447 replacement = lines[index]
448 448 original = node.bin(replacement[:40])
449 449 succ = [
450 450 node.bin(replacement[i : i + 40])
451 451 for i in range(40, len(replacement), 40)
452 452 ]
453 453 replacements.append((original, succ))
454 454 index += 1
455 455
456 456 backupfile = lines[index]
457 457 index += 1
458 458
459 459 fp.close()
460 460
461 461 return parentctxnode, rules, keep, topmost, replacements, backupfile
462 462
463 463 def clear(self):
464 464 if self.inprogress():
465 465 self.repo.vfs.unlink(b'histedit-state')
466 466
467 467 def inprogress(self):
468 468 return self.repo.vfs.exists(b'histedit-state')
469 469
470 470
471 471 class histeditaction(object):
472 472 def __init__(self, state, node):
473 473 self.state = state
474 474 self.repo = state.repo
475 475 self.node = node
476 476
477 477 @classmethod
478 478 def fromrule(cls, state, rule):
479 479 """Parses the given rule, returning an instance of the histeditaction.
480 480 """
481 481 ruleid = rule.strip().split(b' ', 1)[0]
482 482 # ruleid can be anything from rev numbers, hashes, "bookmarks" etc
483 483 # Check for validation of rule ids and get the rulehash
484 484 try:
485 485 rev = node.bin(ruleid)
486 486 except TypeError:
487 487 try:
488 488 _ctx = scmutil.revsingle(state.repo, ruleid)
489 489 rulehash = _ctx.hex()
490 490 rev = node.bin(rulehash)
491 491 except error.RepoLookupError:
492 492 raise error.ParseError(_(b"invalid changeset %s") % ruleid)
493 493 return cls(state, rev)
494 494
495 495 def verify(self, prev, expected, seen):
496 496 """ Verifies semantic correctness of the rule"""
497 497 repo = self.repo
498 498 ha = node.hex(self.node)
499 499 self.node = scmutil.resolvehexnodeidprefix(repo, ha)
500 500 if self.node is None:
501 501 raise error.ParseError(_(b'unknown changeset %s listed') % ha[:12])
502 502 self._verifynodeconstraints(prev, expected, seen)
503 503
504 504 def _verifynodeconstraints(self, prev, expected, seen):
505 505 # by default command need a node in the edited list
506 506 if self.node not in expected:
507 507 raise error.ParseError(
508 508 _(b'%s "%s" changeset was not a candidate')
509 509 % (self.verb, node.short(self.node)),
510 510 hint=_(b'only use listed changesets'),
511 511 )
512 512 # and only one command per node
513 513 if self.node in seen:
514 514 raise error.ParseError(
515 515 _(b'duplicated command for changeset %s')
516 516 % node.short(self.node)
517 517 )
518 518
519 519 def torule(self):
520 520 """build a histedit rule line for an action
521 521
522 522 by default lines are in the form:
523 523 <hash> <rev> <summary>
524 524 """
525 525 ctx = self.repo[self.node]
526 526 ui = self.repo.ui
527 527 summary = (
528 528 cmdutil.rendertemplate(
529 529 ctx, ui.config(b'histedit', b'summary-template')
530 530 )
531 531 or b''
532 532 )
533 533 summary = summary.splitlines()[0]
534 534 line = b'%s %s %s' % (self.verb, ctx, summary)
535 535 # trim to 75 columns by default so it's not stupidly wide in my editor
536 536 # (the 5 more are left for verb)
537 537 maxlen = self.repo.ui.configint(b'histedit', b'linelen')
538 538 maxlen = max(maxlen, 22) # avoid truncating hash
539 539 return stringutil.ellipsis(line, maxlen)
540 540
541 541 def tostate(self):
542 542 """Print an action in format used by histedit state files
543 543 (the first line is a verb, the remainder is the second)
544 544 """
545 545 return b"%s\n%s" % (self.verb, node.hex(self.node))
546 546
547 547 def run(self):
548 548 """Runs the action. The default behavior is simply apply the action's
549 549 rulectx onto the current parentctx."""
550 550 self.applychange()
551 551 self.continuedirty()
552 552 return self.continueclean()
553 553
554 554 def applychange(self):
555 555 """Applies the changes from this action's rulectx onto the current
556 556 parentctx, but does not commit them."""
557 557 repo = self.repo
558 558 rulectx = repo[self.node]
559 559 repo.ui.pushbuffer(error=True, labeled=True)
560 560 hg.update(repo, self.state.parentctxnode, quietempty=True)
561 561 repo.ui.popbuffer()
562 562 stats = applychanges(repo.ui, repo, rulectx, {})
563 563 repo.dirstate.setbranch(rulectx.branch())
564 564 if stats.unresolvedcount:
565 565 raise error.InterventionRequired(
566 566 _(b'Fix up the change (%s %s)')
567 567 % (self.verb, node.short(self.node)),
568 568 hint=_(b'hg histedit --continue to resume'),
569 569 )
570 570
571 571 def continuedirty(self):
572 572 """Continues the action when changes have been applied to the working
573 573 copy. The default behavior is to commit the dirty changes."""
574 574 repo = self.repo
575 575 rulectx = repo[self.node]
576 576
577 577 editor = self.commiteditor()
578 578 commit = commitfuncfor(repo, rulectx)
579 579 if repo.ui.configbool(b'rewrite', b'update-timestamp'):
580 580 date = dateutil.makedate()
581 581 else:
582 582 date = rulectx.date()
583 583 commit(
584 584 text=rulectx.description(),
585 585 user=rulectx.user(),
586 586 date=date,
587 587 extra=rulectx.extra(),
588 588 editor=editor,
589 589 )
590 590
591 591 def commiteditor(self):
592 592 """The editor to be used to edit the commit message."""
593 593 return False
594 594
595 595 def continueclean(self):
596 596 """Continues the action when the working copy is clean. The default
597 597 behavior is to accept the current commit as the new version of the
598 598 rulectx."""
599 599 ctx = self.repo[b'.']
600 600 if ctx.node() == self.state.parentctxnode:
601 601 self.repo.ui.warn(
602 602 _(b'%s: skipping changeset (no changes)\n')
603 603 % node.short(self.node)
604 604 )
605 605 return ctx, [(self.node, tuple())]
606 606 if ctx.node() == self.node:
607 607 # Nothing changed
608 608 return ctx, []
609 609 return ctx, [(self.node, (ctx.node(),))]
610 610
611 611
612 612 def commitfuncfor(repo, src):
613 613 """Build a commit function for the replacement of <src>
614 614
615 615 This function ensure we apply the same treatment to all changesets.
616 616
617 617 - Add a 'histedit_source' entry in extra.
618 618
619 619 Note that fold has its own separated logic because its handling is a bit
620 620 different and not easily factored out of the fold method.
621 621 """
622 622 phasemin = src.phase()
623 623
624 624 def commitfunc(**kwargs):
625 625 overrides = {(b'phases', b'new-commit'): phasemin}
626 626 with repo.ui.configoverride(overrides, b'histedit'):
627 627 extra = kwargs.get('extra', {}).copy()
628 628 extra[b'histedit_source'] = src.hex()
629 629 kwargs['extra'] = extra
630 630 return repo.commit(**kwargs)
631 631
632 632 return commitfunc
633 633
634 634
635 635 def applychanges(ui, repo, ctx, opts):
636 636 """Merge changeset from ctx (only) in the current working directory"""
637 637 wcpar = repo.dirstate.p1()
638 638 if ctx.p1().node() == wcpar:
639 639 # edits are "in place" we do not need to make any merge,
640 640 # just applies changes on parent for editing
641 641 ui.pushbuffer()
642 642 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
643 643 stats = mergemod.updateresult(0, 0, 0, 0)
644 644 ui.popbuffer()
645 645 else:
646 646 try:
647 647 # ui.forcemerge is an internal variable, do not document
648 648 repo.ui.setconfig(
649 649 b'ui', b'forcemerge', opts.get(b'tool', b''), b'histedit'
650 650 )
651 651 stats = mergemod.graft(repo, ctx, ctx.p1(), [b'local', b'histedit'])
652 652 finally:
653 653 repo.ui.setconfig(b'ui', b'forcemerge', b'', b'histedit')
654 654 return stats
655 655
656 656
657 657 def collapse(repo, firstctx, lastctx, commitopts, skipprompt=False):
658 658 """collapse the set of revisions from first to last as new one.
659 659
660 660 Expected commit options are:
661 661 - message
662 662 - date
663 663 - username
664 664 Commit message is edited in all cases.
665 665
666 666 This function works in memory."""
667 667 ctxs = list(repo.set(b'%d::%d', firstctx.rev(), lastctx.rev()))
668 668 if not ctxs:
669 669 return None
670 670 for c in ctxs:
671 671 if not c.mutable():
672 672 raise error.ParseError(
673 673 _(b"cannot fold into public change %s") % node.short(c.node())
674 674 )
675 675 base = firstctx.p1()
676 676
677 677 # commit a new version of the old changeset, including the update
678 678 # collect all files which might be affected
679 679 files = set()
680 680 for ctx in ctxs:
681 681 files.update(ctx.files())
682 682
683 683 # Recompute copies (avoid recording a -> b -> a)
684 684 copied = copies.pathcopies(base, lastctx)
685 685
686 686 # prune files which were reverted by the updates
687 687 files = [f for f in files if not cmdutil.samefile(f, lastctx, base)]
688 688 # commit version of these files as defined by head
689 689 headmf = lastctx.manifest()
690 690
691 691 def filectxfn(repo, ctx, path):
692 692 if path in headmf:
693 693 fctx = lastctx[path]
694 694 flags = fctx.flags()
695 695 mctx = context.memfilectx(
696 696 repo,
697 697 ctx,
698 698 fctx.path(),
699 699 fctx.data(),
700 700 islink=b'l' in flags,
701 701 isexec=b'x' in flags,
702 702 copysource=copied.get(path),
703 703 )
704 704 return mctx
705 705 return None
706 706
707 707 if commitopts.get(b'message'):
708 708 message = commitopts[b'message']
709 709 else:
710 710 message = firstctx.description()
711 711 user = commitopts.get(b'user')
712 712 date = commitopts.get(b'date')
713 713 extra = commitopts.get(b'extra')
714 714
715 715 parents = (firstctx.p1().node(), firstctx.p2().node())
716 716 editor = None
717 717 if not skipprompt:
718 718 editor = cmdutil.getcommiteditor(edit=True, editform=b'histedit.fold')
719 719 new = context.memctx(
720 720 repo,
721 721 parents=parents,
722 722 text=message,
723 723 files=files,
724 724 filectxfn=filectxfn,
725 725 user=user,
726 726 date=date,
727 727 extra=extra,
728 728 editor=editor,
729 729 )
730 730 return repo.commitctx(new)
731 731
732 732
733 733 def _isdirtywc(repo):
734 734 return repo[None].dirty(missing=True)
735 735
736 736
737 737 def abortdirty():
738 738 raise error.Abort(
739 739 _(b'working copy has pending changes'),
740 740 hint=_(
741 741 b'amend, commit, or revert them and run histedit '
742 742 b'--continue, or abort with histedit --abort'
743 743 ),
744 744 )
745 745
746 746
747 747 def action(verbs, message, priority=False, internal=False):
748 748 def wrap(cls):
749 749 assert not priority or not internal
750 750 verb = verbs[0]
751 751 if priority:
752 752 primaryactions.add(verb)
753 753 elif internal:
754 754 internalactions.add(verb)
755 755 elif len(verbs) > 1:
756 756 secondaryactions.add(verb)
757 757 else:
758 758 tertiaryactions.add(verb)
759 759
760 760 cls.verb = verb
761 761 cls.verbs = verbs
762 762 cls.message = message
763 763 for verb in verbs:
764 764 actiontable[verb] = cls
765 765 return cls
766 766
767 767 return wrap
768 768
769 769
770 770 @action([b'pick', b'p'], _(b'use commit'), priority=True)
771 771 class pick(histeditaction):
772 772 def run(self):
773 773 rulectx = self.repo[self.node]
774 774 if rulectx.p1().node() == self.state.parentctxnode:
775 775 self.repo.ui.debug(b'node %s unchanged\n' % node.short(self.node))
776 776 return rulectx, []
777 777
778 778 return super(pick, self).run()
779 779
780 780
781 781 @action([b'edit', b'e'], _(b'use commit, but stop for amending'), priority=True)
782 782 class edit(histeditaction):
783 783 def run(self):
784 784 repo = self.repo
785 785 rulectx = repo[self.node]
786 786 hg.update(repo, self.state.parentctxnode, quietempty=True)
787 787 applychanges(repo.ui, repo, rulectx, {})
788 788 raise error.InterventionRequired(
789 789 _(b'Editing (%s), you may commit or record as needed now.')
790 790 % node.short(self.node),
791 791 hint=_(b'hg histedit --continue to resume'),
792 792 )
793 793
794 794 def commiteditor(self):
795 795 return cmdutil.getcommiteditor(edit=True, editform=b'histedit.edit')
796 796
797 797
798 798 @action([b'fold', b'f'], _(b'use commit, but combine it with the one above'))
799 799 class fold(histeditaction):
800 800 def verify(self, prev, expected, seen):
801 801 """ Verifies semantic correctness of the fold rule"""
802 802 super(fold, self).verify(prev, expected, seen)
803 803 repo = self.repo
804 804 if not prev:
805 805 c = repo[self.node].p1()
806 806 elif not prev.verb in (b'pick', b'base'):
807 807 return
808 808 else:
809 809 c = repo[prev.node]
810 810 if not c.mutable():
811 811 raise error.ParseError(
812 812 _(b"cannot fold into public change %s") % node.short(c.node())
813 813 )
814 814
815 815 def continuedirty(self):
816 816 repo = self.repo
817 817 rulectx = repo[self.node]
818 818
819 819 commit = commitfuncfor(repo, rulectx)
820 820 commit(
821 821 text=b'fold-temp-revision %s' % node.short(self.node),
822 822 user=rulectx.user(),
823 823 date=rulectx.date(),
824 824 extra=rulectx.extra(),
825 825 )
826 826
827 827 def continueclean(self):
828 828 repo = self.repo
829 829 ctx = repo[b'.']
830 830 rulectx = repo[self.node]
831 831 parentctxnode = self.state.parentctxnode
832 832 if ctx.node() == parentctxnode:
833 833 repo.ui.warn(_(b'%s: empty changeset\n') % node.short(self.node))
834 834 return ctx, [(self.node, (parentctxnode,))]
835 835
836 836 parentctx = repo[parentctxnode]
837 837 newcommits = set(
838 838 c.node()
839 839 for c in repo.set(b'(%d::. - %d)', parentctx.rev(), parentctx.rev())
840 840 )
841 841 if not newcommits:
842 842 repo.ui.warn(
843 843 _(
844 844 b'%s: cannot fold - working copy is not a '
845 845 b'descendant of previous commit %s\n'
846 846 )
847 847 % (node.short(self.node), node.short(parentctxnode))
848 848 )
849 849 return ctx, [(self.node, (ctx.node(),))]
850 850
851 851 middlecommits = newcommits.copy()
852 852 middlecommits.discard(ctx.node())
853 853
854 854 return self.finishfold(
855 855 repo.ui, repo, parentctx, rulectx, ctx.node(), middlecommits
856 856 )
857 857
858 858 def skipprompt(self):
859 859 """Returns true if the rule should skip the message editor.
860 860
861 861 For example, 'fold' wants to show an editor, but 'rollup'
862 862 doesn't want to.
863 863 """
864 864 return False
865 865
866 866 def mergedescs(self):
867 867 """Returns true if the rule should merge messages of multiple changes.
868 868
869 869 This exists mainly so that 'rollup' rules can be a subclass of
870 870 'fold'.
871 871 """
872 872 return True
873 873
874 874 def firstdate(self):
875 875 """Returns true if the rule should preserve the date of the first
876 876 change.
877 877
878 878 This exists mainly so that 'rollup' rules can be a subclass of
879 879 'fold'.
880 880 """
881 881 return False
882 882
883 883 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
884 884 parent = ctx.p1().node()
885 885 hg.updaterepo(repo, parent, overwrite=False)
886 886 ### prepare new commit data
887 887 commitopts = {}
888 888 commitopts[b'user'] = ctx.user()
889 889 # commit message
890 890 if not self.mergedescs():
891 891 newmessage = ctx.description()
892 892 else:
893 893 newmessage = (
894 894 b'\n***\n'.join(
895 895 [ctx.description()]
896 896 + [repo[r].description() for r in internalchanges]
897 897 + [oldctx.description()]
898 898 )
899 899 + b'\n'
900 900 )
901 901 commitopts[b'message'] = newmessage
902 902 # date
903 903 if self.firstdate():
904 904 commitopts[b'date'] = ctx.date()
905 905 else:
906 906 commitopts[b'date'] = max(ctx.date(), oldctx.date())
907 907 # if date is to be updated to current
908 908 if ui.configbool(b'rewrite', b'update-timestamp'):
909 909 commitopts[b'date'] = dateutil.makedate()
910 910
911 911 extra = ctx.extra().copy()
912 912 # histedit_source
913 913 # note: ctx is likely a temporary commit but that the best we can do
914 914 # here. This is sufficient to solve issue3681 anyway.
915 915 extra[b'histedit_source'] = b'%s,%s' % (ctx.hex(), oldctx.hex())
916 916 commitopts[b'extra'] = extra
917 917 phasemin = max(ctx.phase(), oldctx.phase())
918 918 overrides = {(b'phases', b'new-commit'): phasemin}
919 919 with repo.ui.configoverride(overrides, b'histedit'):
920 920 n = collapse(
921 921 repo,
922 922 ctx,
923 923 repo[newnode],
924 924 commitopts,
925 925 skipprompt=self.skipprompt(),
926 926 )
927 927 if n is None:
928 928 return ctx, []
929 929 hg.updaterepo(repo, n, overwrite=False)
930 930 replacements = [
931 931 (oldctx.node(), (newnode,)),
932 932 (ctx.node(), (n,)),
933 933 (newnode, (n,)),
934 934 ]
935 935 for ich in internalchanges:
936 936 replacements.append((ich, (n,)))
937 937 return repo[n], replacements
938 938
939 939
940 940 @action(
941 941 [b'base', b'b'],
942 942 _(b'checkout changeset and apply further changesets from there'),
943 943 )
944 944 class base(histeditaction):
945 945 def run(self):
946 946 if self.repo[b'.'].node() != self.node:
947 947 mergemod.update(self.repo, self.node, branchmerge=False, force=True)
948 948 return self.continueclean()
949 949
950 950 def continuedirty(self):
951 951 abortdirty()
952 952
953 953 def continueclean(self):
954 954 basectx = self.repo[b'.']
955 955 return basectx, []
956 956
957 957 def _verifynodeconstraints(self, prev, expected, seen):
958 958 # base can only be use with a node not in the edited set
959 959 if self.node in expected:
960 960 msg = _(b'%s "%s" changeset was an edited list candidate')
961 961 raise error.ParseError(
962 962 msg % (self.verb, node.short(self.node)),
963 963 hint=_(b'base must only use unlisted changesets'),
964 964 )
965 965
966 966
967 967 @action(
968 968 [b'_multifold'],
969 969 _(
970 970 """fold subclass used for when multiple folds happen in a row
971 971
972 972 We only want to fire the editor for the folded message once when
973 973 (say) four changes are folded down into a single change. This is
974 974 similar to rollup, but we should preserve both messages so that
975 975 when the last fold operation runs we can show the user all the
976 976 commit messages in their editor.
977 977 """
978 978 ),
979 979 internal=True,
980 980 )
981 981 class _multifold(fold):
982 982 def skipprompt(self):
983 983 return True
984 984
985 985
986 986 @action(
987 987 [b"roll", b"r"],
988 988 _(b"like fold, but discard this commit's description and date"),
989 989 )
990 990 class rollup(fold):
991 991 def mergedescs(self):
992 992 return False
993 993
994 994 def skipprompt(self):
995 995 return True
996 996
997 997 def firstdate(self):
998 998 return True
999 999
1000 1000
1001 1001 @action([b"drop", b"d"], _(b'remove commit from history'))
1002 1002 class drop(histeditaction):
1003 1003 def run(self):
1004 1004 parentctx = self.repo[self.state.parentctxnode]
1005 1005 return parentctx, [(self.node, tuple())]
1006 1006
1007 1007
1008 1008 @action(
1009 1009 [b"mess", b"m"],
1010 1010 _(b'edit commit message without changing commit content'),
1011 1011 priority=True,
1012 1012 )
1013 1013 class message(histeditaction):
1014 1014 def commiteditor(self):
1015 1015 return cmdutil.getcommiteditor(edit=True, editform=b'histedit.mess')
1016 1016
1017 1017
1018 1018 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
1019 1019 """utility function to find the first outgoing changeset
1020 1020
1021 1021 Used by initialization code"""
1022 1022 if opts is None:
1023 1023 opts = {}
1024 1024 dest = ui.expandpath(remote or b'default-push', remote or b'default')
1025 1025 dest, branches = hg.parseurl(dest, None)[:2]
1026 1026 ui.status(_(b'comparing with %s\n') % util.hidepassword(dest))
1027 1027
1028 1028 revs, checkout = hg.addbranchrevs(repo, repo, branches, None)
1029 1029 other = hg.peer(repo, opts, dest)
1030 1030
1031 1031 if revs:
1032 1032 revs = [repo.lookup(rev) for rev in revs]
1033 1033
1034 1034 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
1035 1035 if not outgoing.missing:
1036 1036 raise error.Abort(_(b'no outgoing ancestors'))
1037 1037 roots = list(repo.revs(b"roots(%ln)", outgoing.missing))
1038 1038 if len(roots) > 1:
1039 1039 msg = _(b'there are ambiguous outgoing revisions')
1040 1040 hint = _(b"see 'hg help histedit' for more detail")
1041 1041 raise error.Abort(msg, hint=hint)
1042 1042 return repo[roots[0]].node()
1043 1043
1044 1044
1045 1045 # Curses Support
1046 1046 try:
1047 1047 import curses
1048 1048 except ImportError:
1049 1049 curses = None
1050 1050
1051 1051 KEY_LIST = [b'pick', b'edit', b'fold', b'drop', b'mess', b'roll']
1052 1052 ACTION_LABELS = {
1053 1053 b'fold': b'^fold',
1054 1054 b'roll': b'^roll',
1055 1055 }
1056 1056
1057 1057 COLOR_HELP, COLOR_SELECTED, COLOR_OK, COLOR_WARN, COLOR_CURRENT = 1, 2, 3, 4, 5
1058 1058 COLOR_DIFF_ADD_LINE, COLOR_DIFF_DEL_LINE, COLOR_DIFF_OFFSET = 6, 7, 8
1059 1059 COLOR_ROLL, COLOR_ROLL_CURRENT, COLOR_ROLL_SELECTED = 9, 10, 11
1060 1060
1061 1061 E_QUIT, E_HISTEDIT = 1, 2
1062 1062 E_PAGEDOWN, E_PAGEUP, E_LINEUP, E_LINEDOWN, E_RESIZE = 3, 4, 5, 6, 7
1063 1063 MODE_INIT, MODE_PATCH, MODE_RULES, MODE_HELP = 0, 1, 2, 3
1064 1064
1065 1065 KEYTABLE = {
1066 1066 b'global': {
1067 1067 b'h': b'next-action',
1068 1068 b'KEY_RIGHT': b'next-action',
1069 1069 b'l': b'prev-action',
1070 1070 b'KEY_LEFT': b'prev-action',
1071 1071 b'q': b'quit',
1072 1072 b'c': b'histedit',
1073 1073 b'C': b'histedit',
1074 1074 b'v': b'showpatch',
1075 1075 b'?': b'help',
1076 1076 },
1077 1077 MODE_RULES: {
1078 1078 b'd': b'action-drop',
1079 1079 b'e': b'action-edit',
1080 1080 b'f': b'action-fold',
1081 1081 b'm': b'action-mess',
1082 1082 b'p': b'action-pick',
1083 1083 b'r': b'action-roll',
1084 1084 b' ': b'select',
1085 1085 b'j': b'down',
1086 1086 b'k': b'up',
1087 1087 b'KEY_DOWN': b'down',
1088 1088 b'KEY_UP': b'up',
1089 1089 b'J': b'move-down',
1090 1090 b'K': b'move-up',
1091 1091 b'KEY_NPAGE': b'move-down',
1092 1092 b'KEY_PPAGE': b'move-up',
1093 1093 b'0': b'goto', # Used for 0..9
1094 1094 },
1095 1095 MODE_PATCH: {
1096 1096 b' ': b'page-down',
1097 1097 b'KEY_NPAGE': b'page-down',
1098 1098 b'KEY_PPAGE': b'page-up',
1099 1099 b'j': b'line-down',
1100 1100 b'k': b'line-up',
1101 1101 b'KEY_DOWN': b'line-down',
1102 1102 b'KEY_UP': b'line-up',
1103 1103 b'J': b'down',
1104 1104 b'K': b'up',
1105 1105 },
1106 1106 MODE_HELP: {},
1107 1107 }
1108 1108
1109 1109
1110 1110 def screen_size():
1111 1111 return struct.unpack(b'hh', fcntl.ioctl(1, termios.TIOCGWINSZ, b' '))
1112 1112
1113 1113
1114 1114 class histeditrule(object):
1115 1115 def __init__(self, ctx, pos, action=b'pick'):
1116 1116 self.ctx = ctx
1117 1117 self.action = action
1118 1118 self.origpos = pos
1119 1119 self.pos = pos
1120 1120 self.conflicts = []
1121 1121
1122 1122 def __bytes__(self):
1123 1123 # Example display of several histeditrules:
1124 1124 #
1125 1125 # #10 pick 316392:06a16c25c053 add option to skip tests
1126 1126 # #11 ^roll 316393:71313c964cc5 <RED>oops a fixup commit</RED>
1127 1127 # #12 pick 316394:ab31f3973b0d include mfbt for mozilla-config.h
1128 1128 # #13 ^fold 316395:14ce5803f4c3 fix warnings
1129 1129 #
1130 1130 # The carets point to the changeset being folded into ("roll this
1131 1131 # changeset into the changeset above").
1132 1132 return b'%s%s' % (self.prefix, self.desc)
1133 1133
1134 1134 __str__ = encoding.strmethod(__bytes__)
1135 1135
1136 1136 @property
1137 1137 def prefix(self):
1138 1138 # Some actions ('fold' and 'roll') combine a patch with a
1139 1139 # previous one. Add a marker showing which patch they apply
1140 1140 # to.
1141 1141 action = ACTION_LABELS.get(self.action, self.action)
1142 1142
1143 1143 h = self.ctx.hex()[0:12]
1144 1144 r = self.ctx.rev()
1145 1145
1146 1146 return b"#%s %s %d:%s " % (
1147 1147 (b'%d' % self.origpos).ljust(2),
1148 1148 action.ljust(6),
1149 1149 r,
1150 1150 h,
1151 1151 )
1152 1152
1153 1153 @property
1154 1154 def desc(self):
1155 1155 # This is split off from the prefix property so that we can
1156 1156 # separately make the description for 'roll' red (since it
1157 1157 # will get discarded).
1158 1158 return self.ctx.description().splitlines()[0].strip()
1159 1159
1160 1160 def checkconflicts(self, other):
1161 1161 if other.pos > self.pos and other.origpos <= self.origpos:
1162 1162 if set(other.ctx.files()) & set(self.ctx.files()) != set():
1163 1163 self.conflicts.append(other)
1164 1164 return self.conflicts
1165 1165
1166 1166 if other in self.conflicts:
1167 1167 self.conflicts.remove(other)
1168 1168 return self.conflicts
1169 1169
1170 1170
1171 1171 # ============ EVENTS ===============
1172 1172 def movecursor(state, oldpos, newpos):
1173 1173 '''Change the rule/changeset that the cursor is pointing to, regardless of
1174 1174 current mode (you can switch between patches from the view patch window).'''
1175 1175 state[b'pos'] = newpos
1176 1176
1177 1177 mode, _ = state[b'mode']
1178 1178 if mode == MODE_RULES:
1179 1179 # Scroll through the list by updating the view for MODE_RULES, so that
1180 1180 # even if we are not currently viewing the rules, switching back will
1181 1181 # result in the cursor's rule being visible.
1182 1182 modestate = state[b'modes'][MODE_RULES]
1183 1183 if newpos < modestate[b'line_offset']:
1184 1184 modestate[b'line_offset'] = newpos
1185 1185 elif newpos > modestate[b'line_offset'] + state[b'page_height'] - 1:
1186 1186 modestate[b'line_offset'] = newpos - state[b'page_height'] + 1
1187 1187
1188 1188 # Reset the patch view region to the top of the new patch.
1189 1189 state[b'modes'][MODE_PATCH][b'line_offset'] = 0
1190 1190
1191 1191
1192 1192 def changemode(state, mode):
1193 1193 curmode, _ = state[b'mode']
1194 1194 state[b'mode'] = (mode, curmode)
1195 1195 if mode == MODE_PATCH:
1196 1196 state[b'modes'][MODE_PATCH][b'patchcontents'] = patchcontents(state)
1197 1197
1198 1198
1199 1199 def makeselection(state, pos):
1200 1200 state[b'selected'] = pos
1201 1201
1202 1202
1203 1203 def swap(state, oldpos, newpos):
1204 1204 """Swap two positions and calculate necessary conflicts in
1205 1205 O(|newpos-oldpos|) time"""
1206 1206
1207 1207 rules = state[b'rules']
1208 1208 assert 0 <= oldpos < len(rules) and 0 <= newpos < len(rules)
1209 1209
1210 1210 rules[oldpos], rules[newpos] = rules[newpos], rules[oldpos]
1211 1211
1212 1212 # TODO: swap should not know about histeditrule's internals
1213 1213 rules[newpos].pos = newpos
1214 1214 rules[oldpos].pos = oldpos
1215 1215
1216 1216 start = min(oldpos, newpos)
1217 1217 end = max(oldpos, newpos)
1218 1218 for r in pycompat.xrange(start, end + 1):
1219 1219 rules[newpos].checkconflicts(rules[r])
1220 1220 rules[oldpos].checkconflicts(rules[r])
1221 1221
1222 1222 if state[b'selected']:
1223 1223 makeselection(state, newpos)
1224 1224
1225 1225
1226 1226 def changeaction(state, pos, action):
1227 1227 """Change the action state on the given position to the new action"""
1228 1228 rules = state[b'rules']
1229 1229 assert 0 <= pos < len(rules)
1230 1230 rules[pos].action = action
1231 1231
1232 1232
1233 1233 def cycleaction(state, pos, next=False):
1234 1234 """Changes the action state the next or the previous action from
1235 1235 the action list"""
1236 1236 rules = state[b'rules']
1237 1237 assert 0 <= pos < len(rules)
1238 1238 current = rules[pos].action
1239 1239
1240 1240 assert current in KEY_LIST
1241 1241
1242 1242 index = KEY_LIST.index(current)
1243 1243 if next:
1244 1244 index += 1
1245 1245 else:
1246 1246 index -= 1
1247 1247 changeaction(state, pos, KEY_LIST[index % len(KEY_LIST)])
1248 1248
1249 1249
1250 1250 def changeview(state, delta, unit):
1251 1251 '''Change the region of whatever is being viewed (a patch or the list of
1252 1252 changesets). 'delta' is an amount (+/- 1) and 'unit' is 'page' or 'line'.'''
1253 1253 mode, _ = state[b'mode']
1254 1254 if mode != MODE_PATCH:
1255 1255 return
1256 1256 mode_state = state[b'modes'][mode]
1257 1257 num_lines = len(mode_state[b'patchcontents'])
1258 1258 page_height = state[b'page_height']
1259 1259 unit = page_height if unit == b'page' else 1
1260 1260 num_pages = 1 + (num_lines - 1) / page_height
1261 1261 max_offset = (num_pages - 1) * page_height
1262 1262 newline = mode_state[b'line_offset'] + delta * unit
1263 1263 mode_state[b'line_offset'] = max(0, min(max_offset, newline))
1264 1264
1265 1265
1266 1266 def event(state, ch):
1267 1267 """Change state based on the current character input
1268 1268
1269 1269 This takes the current state and based on the current character input from
1270 1270 the user we change the state.
1271 1271 """
1272 1272 selected = state[b'selected']
1273 1273 oldpos = state[b'pos']
1274 1274 rules = state[b'rules']
1275 1275
1276 1276 if ch in (curses.KEY_RESIZE, b"KEY_RESIZE"):
1277 1277 return E_RESIZE
1278 1278
1279 1279 lookup_ch = ch
1280 1280 if ch is not None and b'0' <= ch <= b'9':
1281 1281 lookup_ch = b'0'
1282 1282
1283 1283 curmode, prevmode = state[b'mode']
1284 1284 action = KEYTABLE[curmode].get(
1285 1285 lookup_ch, KEYTABLE[b'global'].get(lookup_ch)
1286 1286 )
1287 1287 if action is None:
1288 1288 return
1289 1289 if action in (b'down', b'move-down'):
1290 1290 newpos = min(oldpos + 1, len(rules) - 1)
1291 1291 movecursor(state, oldpos, newpos)
1292 1292 if selected is not None or action == b'move-down':
1293 1293 swap(state, oldpos, newpos)
1294 1294 elif action in (b'up', b'move-up'):
1295 1295 newpos = max(0, oldpos - 1)
1296 1296 movecursor(state, oldpos, newpos)
1297 1297 if selected is not None or action == b'move-up':
1298 1298 swap(state, oldpos, newpos)
1299 1299 elif action == b'next-action':
1300 1300 cycleaction(state, oldpos, next=True)
1301 1301 elif action == b'prev-action':
1302 1302 cycleaction(state, oldpos, next=False)
1303 1303 elif action == b'select':
1304 1304 selected = oldpos if selected is None else None
1305 1305 makeselection(state, selected)
1306 1306 elif action == b'goto' and int(ch) < len(rules) and len(rules) <= 10:
1307 1307 newrule = next((r for r in rules if r.origpos == int(ch)))
1308 1308 movecursor(state, oldpos, newrule.pos)
1309 1309 if selected is not None:
1310 1310 swap(state, oldpos, newrule.pos)
1311 1311 elif action.startswith(b'action-'):
1312 1312 changeaction(state, oldpos, action[7:])
1313 1313 elif action == b'showpatch':
1314 1314 changemode(state, MODE_PATCH if curmode != MODE_PATCH else prevmode)
1315 1315 elif action == b'help':
1316 1316 changemode(state, MODE_HELP if curmode != MODE_HELP else prevmode)
1317 1317 elif action == b'quit':
1318 1318 return E_QUIT
1319 1319 elif action == b'histedit':
1320 1320 return E_HISTEDIT
1321 1321 elif action == b'page-down':
1322 1322 return E_PAGEDOWN
1323 1323 elif action == b'page-up':
1324 1324 return E_PAGEUP
1325 1325 elif action == b'line-down':
1326 1326 return E_LINEDOWN
1327 1327 elif action == b'line-up':
1328 1328 return E_LINEUP
1329 1329
1330 1330
1331 1331 def makecommands(rules):
1332 1332 """Returns a list of commands consumable by histedit --commands based on
1333 1333 our list of rules"""
1334 1334 commands = []
1335 1335 for rules in rules:
1336 1336 commands.append(b'%s %s\n' % (rules.action, rules.ctx))
1337 1337 return commands
1338 1338
1339 1339
1340 1340 def addln(win, y, x, line, color=None):
1341 1341 """Add a line to the given window left padding but 100% filled with
1342 1342 whitespace characters, so that the color appears on the whole line"""
1343 1343 maxy, maxx = win.getmaxyx()
1344 1344 length = maxx - 1 - x
1345 1345 line = bytes(line).ljust(length)[:length]
1346 1346 if y < 0:
1347 1347 y = maxy + y
1348 1348 if x < 0:
1349 1349 x = maxx + x
1350 1350 if color:
1351 1351 win.addstr(y, x, line, color)
1352 1352 else:
1353 1353 win.addstr(y, x, line)
1354 1354
1355 1355
1356 1356 def _trunc_head(line, n):
1357 1357 if len(line) <= n:
1358 1358 return line
1359 1359 return b'> ' + line[-(n - 2) :]
1360 1360
1361 1361
1362 1362 def _trunc_tail(line, n):
1363 1363 if len(line) <= n:
1364 1364 return line
1365 1365 return line[: n - 2] + b' >'
1366 1366
1367 1367
1368 1368 def patchcontents(state):
1369 1369 repo = state[b'repo']
1370 1370 rule = state[b'rules'][state[b'pos']]
1371 1371 displayer = logcmdutil.changesetdisplayer(
1372 1372 repo.ui, repo, {b"patch": True, b"template": b"status"}, buffered=True
1373 1373 )
1374 1374 overrides = {(b'ui', b'verbose'): True}
1375 1375 with repo.ui.configoverride(overrides, source=b'histedit'):
1376 1376 displayer.show(rule.ctx)
1377 1377 displayer.close()
1378 1378 return displayer.hunk[rule.ctx.rev()].splitlines()
1379 1379
1380 1380
1381 1381 def _chisteditmain(repo, rules, stdscr):
1382 1382 try:
1383 1383 curses.use_default_colors()
1384 1384 except curses.error:
1385 1385 pass
1386 1386
1387 1387 # initialize color pattern
1388 1388 curses.init_pair(COLOR_HELP, curses.COLOR_WHITE, curses.COLOR_BLUE)
1389 1389 curses.init_pair(COLOR_SELECTED, curses.COLOR_BLACK, curses.COLOR_WHITE)
1390 1390 curses.init_pair(COLOR_WARN, curses.COLOR_BLACK, curses.COLOR_YELLOW)
1391 1391 curses.init_pair(COLOR_OK, curses.COLOR_BLACK, curses.COLOR_GREEN)
1392 1392 curses.init_pair(COLOR_CURRENT, curses.COLOR_WHITE, curses.COLOR_MAGENTA)
1393 1393 curses.init_pair(COLOR_DIFF_ADD_LINE, curses.COLOR_GREEN, -1)
1394 1394 curses.init_pair(COLOR_DIFF_DEL_LINE, curses.COLOR_RED, -1)
1395 1395 curses.init_pair(COLOR_DIFF_OFFSET, curses.COLOR_MAGENTA, -1)
1396 1396 curses.init_pair(COLOR_ROLL, curses.COLOR_RED, -1)
1397 1397 curses.init_pair(
1398 1398 COLOR_ROLL_CURRENT, curses.COLOR_BLACK, curses.COLOR_MAGENTA
1399 1399 )
1400 1400 curses.init_pair(COLOR_ROLL_SELECTED, curses.COLOR_RED, curses.COLOR_WHITE)
1401 1401
1402 1402 # don't display the cursor
1403 1403 try:
1404 1404 curses.curs_set(0)
1405 1405 except curses.error:
1406 1406 pass
1407 1407
1408 1408 def rendercommit(win, state):
1409 1409 """Renders the commit window that shows the log of the current selected
1410 1410 commit"""
1411 1411 pos = state[b'pos']
1412 1412 rules = state[b'rules']
1413 1413 rule = rules[pos]
1414 1414
1415 1415 ctx = rule.ctx
1416 1416 win.box()
1417 1417
1418 1418 maxy, maxx = win.getmaxyx()
1419 1419 length = maxx - 3
1420 1420
1421 1421 line = b"changeset: %d:%s" % (ctx.rev(), ctx.hex()[:12])
1422 1422 win.addstr(1, 1, line[:length])
1423 1423
1424 1424 line = b"user: %s" % ctx.user()
1425 1425 win.addstr(2, 1, line[:length])
1426 1426
1427 1427 bms = repo.nodebookmarks(ctx.node())
1428 1428 line = b"bookmark: %s" % b' '.join(bms)
1429 1429 win.addstr(3, 1, line[:length])
1430 1430
1431 1431 line = b"summary: %s" % (ctx.description().splitlines()[0])
1432 1432 win.addstr(4, 1, line[:length])
1433 1433
1434 1434 line = b"files: "
1435 1435 win.addstr(5, 1, line)
1436 1436 fnx = 1 + len(line)
1437 1437 fnmaxx = length - fnx + 1
1438 1438 y = 5
1439 1439 fnmaxn = maxy - (1 + y) - 1
1440 1440 files = ctx.files()
1441 1441 for i, line1 in enumerate(files):
1442 1442 if len(files) > fnmaxn and i == fnmaxn - 1:
1443 1443 win.addstr(y, fnx, _trunc_tail(b','.join(files[i:]), fnmaxx))
1444 1444 y = y + 1
1445 1445 break
1446 1446 win.addstr(y, fnx, _trunc_head(line1, fnmaxx))
1447 1447 y = y + 1
1448 1448
1449 1449 conflicts = rule.conflicts
1450 1450 if len(conflicts) > 0:
1451 1451 conflictstr = b','.join(map(lambda r: r.ctx.hex()[:12], conflicts))
1452 1452 conflictstr = b"changed files overlap with %s" % conflictstr
1453 1453 else:
1454 1454 conflictstr = b'no overlap'
1455 1455
1456 1456 win.addstr(y, 1, conflictstr[:length])
1457 1457 win.noutrefresh()
1458 1458
1459 1459 def helplines(mode):
1460 1460 if mode == MODE_PATCH:
1461 1461 help = b"""\
1462 1462 ?: help, k/up: line up, j/down: line down, v: stop viewing patch
1463 1463 pgup: prev page, space/pgdn: next page, c: commit, q: abort
1464 1464 """
1465 1465 else:
1466 1466 help = b"""\
1467 1467 ?: help, k/up: move up, j/down: move down, space: select, v: view patch
1468 1468 d: drop, e: edit, f: fold, m: mess, p: pick, r: roll
1469 1469 pgup/K: move patch up, pgdn/J: move patch down, c: commit, q: abort
1470 1470 """
1471 1471 return help.splitlines()
1472 1472
1473 1473 def renderhelp(win, state):
1474 1474 maxy, maxx = win.getmaxyx()
1475 1475 mode, _ = state[b'mode']
1476 1476 for y, line in enumerate(helplines(mode)):
1477 1477 if y >= maxy:
1478 1478 break
1479 1479 addln(win, y, 0, line, curses.color_pair(COLOR_HELP))
1480 1480 win.noutrefresh()
1481 1481
1482 1482 def renderrules(rulesscr, state):
1483 1483 rules = state[b'rules']
1484 1484 pos = state[b'pos']
1485 1485 selected = state[b'selected']
1486 1486 start = state[b'modes'][MODE_RULES][b'line_offset']
1487 1487
1488 1488 conflicts = [r.ctx for r in rules if r.conflicts]
1489 1489 if len(conflicts) > 0:
1490 1490 line = b"potential conflict in %s" % b','.join(
1491 1491 map(pycompat.bytestr, conflicts)
1492 1492 )
1493 1493 addln(rulesscr, -1, 0, line, curses.color_pair(COLOR_WARN))
1494 1494
1495 1495 for y, rule in enumerate(rules[start:]):
1496 1496 if y >= state[b'page_height']:
1497 1497 break
1498 1498 if len(rule.conflicts) > 0:
1499 1499 rulesscr.addstr(y, 0, b" ", curses.color_pair(COLOR_WARN))
1500 1500 else:
1501 1501 rulesscr.addstr(y, 0, b" ", curses.COLOR_BLACK)
1502 1502
1503 1503 if y + start == selected:
1504 1504 rollcolor = COLOR_ROLL_SELECTED
1505 1505 addln(rulesscr, y, 2, rule, curses.color_pair(COLOR_SELECTED))
1506 1506 elif y + start == pos:
1507 1507 rollcolor = COLOR_ROLL_CURRENT
1508 1508 addln(
1509 1509 rulesscr,
1510 1510 y,
1511 1511 2,
1512 1512 rule,
1513 1513 curses.color_pair(COLOR_CURRENT) | curses.A_BOLD,
1514 1514 )
1515 1515 else:
1516 1516 rollcolor = COLOR_ROLL
1517 1517 addln(rulesscr, y, 2, rule)
1518 1518
1519 1519 if rule.action == b'roll':
1520 1520 rulesscr.addstr(
1521 1521 y,
1522 1522 2 + len(rule.prefix),
1523 1523 rule.desc,
1524 1524 curses.color_pair(rollcolor),
1525 1525 )
1526 1526
1527 1527 rulesscr.noutrefresh()
1528 1528
1529 1529 def renderstring(win, state, output, diffcolors=False):
1530 1530 maxy, maxx = win.getmaxyx()
1531 1531 length = min(maxy - 1, len(output))
1532 1532 for y in range(0, length):
1533 1533 line = output[y]
1534 1534 if diffcolors:
1535 1535 if line and line[0] == b'+':
1536 1536 win.addstr(
1537 1537 y, 0, line, curses.color_pair(COLOR_DIFF_ADD_LINE)
1538 1538 )
1539 1539 elif line and line[0] == b'-':
1540 1540 win.addstr(
1541 1541 y, 0, line, curses.color_pair(COLOR_DIFF_DEL_LINE)
1542 1542 )
1543 1543 elif line.startswith(b'@@ '):
1544 1544 win.addstr(y, 0, line, curses.color_pair(COLOR_DIFF_OFFSET))
1545 1545 else:
1546 1546 win.addstr(y, 0, line)
1547 1547 else:
1548 1548 win.addstr(y, 0, line)
1549 1549 win.noutrefresh()
1550 1550
1551 1551 def renderpatch(win, state):
1552 1552 start = state[b'modes'][MODE_PATCH][b'line_offset']
1553 1553 content = state[b'modes'][MODE_PATCH][b'patchcontents']
1554 1554 renderstring(win, state, content[start:], diffcolors=True)
1555 1555
1556 1556 def layout(mode):
1557 1557 maxy, maxx = stdscr.getmaxyx()
1558 1558 helplen = len(helplines(mode))
1559 1559 return {
1560 1560 b'commit': (12, maxx),
1561 1561 b'help': (helplen, maxx),
1562 1562 b'main': (maxy - helplen - 12, maxx),
1563 1563 }
1564 1564
1565 1565 def drawvertwin(size, y, x):
1566 1566 win = curses.newwin(size[0], size[1], y, x)
1567 1567 y += size[0]
1568 1568 return win, y, x
1569 1569
1570 1570 state = {
1571 1571 b'pos': 0,
1572 1572 b'rules': rules,
1573 1573 b'selected': None,
1574 1574 b'mode': (MODE_INIT, MODE_INIT),
1575 1575 b'page_height': None,
1576 1576 b'modes': {
1577 1577 MODE_RULES: {b'line_offset': 0,},
1578 1578 MODE_PATCH: {b'line_offset': 0,},
1579 1579 },
1580 1580 b'repo': repo,
1581 1581 }
1582 1582
1583 1583 # eventloop
1584 1584 ch = None
1585 1585 stdscr.clear()
1586 1586 stdscr.refresh()
1587 1587 while True:
1588 1588 try:
1589 1589 oldmode, _ = state[b'mode']
1590 1590 if oldmode == MODE_INIT:
1591 1591 changemode(state, MODE_RULES)
1592 1592 e = event(state, ch)
1593 1593
1594 1594 if e == E_QUIT:
1595 1595 return False
1596 1596 if e == E_HISTEDIT:
1597 1597 return state[b'rules']
1598 1598 else:
1599 1599 if e == E_RESIZE:
1600 1600 size = screen_size()
1601 1601 if size != stdscr.getmaxyx():
1602 1602 curses.resizeterm(*size)
1603 1603
1604 1604 curmode, _ = state[b'mode']
1605 1605 sizes = layout(curmode)
1606 1606 if curmode != oldmode:
1607 1607 state[b'page_height'] = sizes[b'main'][0]
1608 1608 # Adjust the view to fit the current screen size.
1609 1609 movecursor(state, state[b'pos'], state[b'pos'])
1610 1610
1611 1611 # Pack the windows against the top, each pane spread across the
1612 1612 # full width of the screen.
1613 1613 y, x = (0, 0)
1614 1614 helpwin, y, x = drawvertwin(sizes[b'help'], y, x)
1615 1615 mainwin, y, x = drawvertwin(sizes[b'main'], y, x)
1616 1616 commitwin, y, x = drawvertwin(sizes[b'commit'], y, x)
1617 1617
1618 1618 if e in (E_PAGEDOWN, E_PAGEUP, E_LINEDOWN, E_LINEUP):
1619 1619 if e == E_PAGEDOWN:
1620 1620 changeview(state, +1, b'page')
1621 1621 elif e == E_PAGEUP:
1622 1622 changeview(state, -1, b'page')
1623 1623 elif e == E_LINEDOWN:
1624 1624 changeview(state, +1, b'line')
1625 1625 elif e == E_LINEUP:
1626 1626 changeview(state, -1, b'line')
1627 1627
1628 1628 # start rendering
1629 1629 commitwin.erase()
1630 1630 helpwin.erase()
1631 1631 mainwin.erase()
1632 1632 if curmode == MODE_PATCH:
1633 1633 renderpatch(mainwin, state)
1634 1634 elif curmode == MODE_HELP:
1635 1635 renderstring(mainwin, state, __doc__.strip().splitlines())
1636 1636 else:
1637 1637 renderrules(mainwin, state)
1638 1638 rendercommit(commitwin, state)
1639 1639 renderhelp(helpwin, state)
1640 1640 curses.doupdate()
1641 1641 # done rendering
1642 1642 ch = encoding.strtolocal(stdscr.getkey())
1643 1643 except curses.error:
1644 1644 pass
1645 1645
1646 1646
1647 1647 def _chistedit(ui, repo, *freeargs, **opts):
1648 1648 """interactively edit changeset history via a curses interface
1649 1649
1650 1650 Provides a ncurses interface to histedit. Press ? in chistedit mode
1651 1651 to see an extensive help. Requires python-curses to be installed."""
1652 1652
1653 1653 if curses is None:
1654 1654 raise error.Abort(_(b"Python curses library required"))
1655 1655
1656 1656 # disable color
1657 1657 ui._colormode = None
1658 1658
1659 1659 try:
1660 1660 keep = opts.get(b'keep')
1661 1661 revs = opts.get(b'rev', [])[:]
1662 1662 cmdutil.checkunfinished(repo)
1663 1663 cmdutil.bailifchanged(repo)
1664 1664
1665 1665 if os.path.exists(os.path.join(repo.path, b'histedit-state')):
1666 1666 raise error.Abort(
1667 1667 _(
1668 1668 b'history edit already in progress, try '
1669 1669 b'--continue or --abort'
1670 1670 )
1671 1671 )
1672 1672 revs.extend(freeargs)
1673 1673 if not revs:
1674 1674 defaultrev = destutil.desthistedit(ui, repo)
1675 1675 if defaultrev is not None:
1676 1676 revs.append(defaultrev)
1677 1677 if len(revs) != 1:
1678 1678 raise error.Abort(
1679 1679 _(b'histedit requires exactly one ancestor revision')
1680 1680 )
1681 1681
1682 1682 rr = list(repo.set(b'roots(%ld)', scmutil.revrange(repo, revs)))
1683 1683 if len(rr) != 1:
1684 1684 raise error.Abort(
1685 1685 _(
1686 1686 b'The specified revisions must have '
1687 1687 b'exactly one common root'
1688 1688 )
1689 1689 )
1690 1690 root = rr[0].node()
1691 1691
1692 1692 topmost = repo.dirstate.p1()
1693 1693 revs = between(repo, root, topmost, keep)
1694 1694 if not revs:
1695 1695 raise error.Abort(
1696 1696 _(b'%s is not an ancestor of working directory')
1697 1697 % node.short(root)
1698 1698 )
1699 1699
1700 1700 ctxs = []
1701 1701 for i, r in enumerate(revs):
1702 1702 ctxs.append(histeditrule(repo[r], i))
1703 1703 # Curses requires setting the locale or it will default to the C
1704 1704 # locale. This sets the locale to the user's default system
1705 1705 # locale.
1706 1706 locale.setlocale(locale.LC_ALL, '')
1707 1707 rc = curses.wrapper(functools.partial(_chisteditmain, repo, ctxs))
1708 1708 curses.echo()
1709 1709 curses.endwin()
1710 1710 if rc is False:
1711 1711 ui.write(_(b"histedit aborted\n"))
1712 1712 return 0
1713 1713 if type(rc) is list:
1714 1714 ui.status(_(b"performing changes\n"))
1715 1715 rules = makecommands(rc)
1716 1716 with repo.vfs(b'chistedit', b'w+') as fp:
1717 1717 for r in rules:
1718 1718 fp.write(r)
1719 1719 opts['commands'] = fp.name
1720 1720 return _texthistedit(ui, repo, *freeargs, **opts)
1721 1721 except KeyboardInterrupt:
1722 1722 pass
1723 1723 return -1
1724 1724
1725 1725
1726 1726 @command(
1727 1727 b'histedit',
1728 1728 [
1729 1729 (
1730 1730 b'',
1731 1731 b'commands',
1732 1732 b'',
1733 1733 _(b'read history edits from the specified file'),
1734 1734 _(b'FILE'),
1735 1735 ),
1736 1736 (b'c', b'continue', False, _(b'continue an edit already in progress')),
1737 1737 (b'', b'edit-plan', False, _(b'edit remaining actions list')),
1738 1738 (
1739 1739 b'k',
1740 1740 b'keep',
1741 1741 False,
1742 1742 _(b"don't strip old nodes after edit is complete"),
1743 1743 ),
1744 1744 (b'', b'abort', False, _(b'abort an edit in progress')),
1745 1745 (b'o', b'outgoing', False, _(b'changesets not found in destination')),
1746 1746 (
1747 1747 b'f',
1748 1748 b'force',
1749 1749 False,
1750 1750 _(b'force outgoing even for unrelated repositories'),
1751 1751 ),
1752 1752 (b'r', b'rev', [], _(b'first revision to be edited'), _(b'REV')),
1753 1753 ]
1754 1754 + cmdutil.formatteropts,
1755 1755 _(b"[OPTIONS] ([ANCESTOR] | --outgoing [URL])"),
1756 1756 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT,
1757 1757 )
1758 1758 def histedit(ui, repo, *freeargs, **opts):
1759 1759 """interactively edit changeset history
1760 1760
1761 1761 This command lets you edit a linear series of changesets (up to
1762 1762 and including the working directory, which should be clean).
1763 1763 You can:
1764 1764
1765 1765 - `pick` to [re]order a changeset
1766 1766
1767 1767 - `drop` to omit changeset
1768 1768
1769 1769 - `mess` to reword the changeset commit message
1770 1770
1771 1771 - `fold` to combine it with the preceding changeset (using the later date)
1772 1772
1773 1773 - `roll` like fold, but discarding this commit's description and date
1774 1774
1775 1775 - `edit` to edit this changeset (preserving date)
1776 1776
1777 1777 - `base` to checkout changeset and apply further changesets from there
1778 1778
1779 1779 There are a number of ways to select the root changeset:
1780 1780
1781 1781 - Specify ANCESTOR directly
1782 1782
1783 1783 - Use --outgoing -- it will be the first linear changeset not
1784 1784 included in destination. (See :hg:`help config.paths.default-push`)
1785 1785
1786 1786 - Otherwise, the value from the "histedit.defaultrev" config option
1787 1787 is used as a revset to select the base revision when ANCESTOR is not
1788 1788 specified. The first revision returned by the revset is used. By
1789 1789 default, this selects the editable history that is unique to the
1790 1790 ancestry of the working directory.
1791 1791
1792 1792 .. container:: verbose
1793 1793
1794 1794 If you use --outgoing, this command will abort if there are ambiguous
1795 1795 outgoing revisions. For example, if there are multiple branches
1796 1796 containing outgoing revisions.
1797 1797
1798 1798 Use "min(outgoing() and ::.)" or similar revset specification
1799 1799 instead of --outgoing to specify edit target revision exactly in
1800 1800 such ambiguous situation. See :hg:`help revsets` for detail about
1801 1801 selecting revisions.
1802 1802
1803 1803 .. container:: verbose
1804 1804
1805 1805 Examples:
1806 1806
1807 1807 - A number of changes have been made.
1808 1808 Revision 3 is no longer needed.
1809 1809
1810 1810 Start history editing from revision 3::
1811 1811
1812 1812 hg histedit -r 3
1813 1813
1814 1814 An editor opens, containing the list of revisions,
1815 1815 with specific actions specified::
1816 1816
1817 1817 pick 5339bf82f0ca 3 Zworgle the foobar
1818 1818 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1819 1819 pick 0a9639fcda9d 5 Morgify the cromulancy
1820 1820
1821 1821 Additional information about the possible actions
1822 1822 to take appears below the list of revisions.
1823 1823
1824 1824 To remove revision 3 from the history,
1825 1825 its action (at the beginning of the relevant line)
1826 1826 is changed to 'drop'::
1827 1827
1828 1828 drop 5339bf82f0ca 3 Zworgle the foobar
1829 1829 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1830 1830 pick 0a9639fcda9d 5 Morgify the cromulancy
1831 1831
1832 1832 - A number of changes have been made.
1833 1833 Revision 2 and 4 need to be swapped.
1834 1834
1835 1835 Start history editing from revision 2::
1836 1836
1837 1837 hg histedit -r 2
1838 1838
1839 1839 An editor opens, containing the list of revisions,
1840 1840 with specific actions specified::
1841 1841
1842 1842 pick 252a1af424ad 2 Blorb a morgwazzle
1843 1843 pick 5339bf82f0ca 3 Zworgle the foobar
1844 1844 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1845 1845
1846 1846 To swap revision 2 and 4, its lines are swapped
1847 1847 in the editor::
1848 1848
1849 1849 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1850 1850 pick 5339bf82f0ca 3 Zworgle the foobar
1851 1851 pick 252a1af424ad 2 Blorb a morgwazzle
1852 1852
1853 1853 Returns 0 on success, 1 if user intervention is required (not only
1854 1854 for intentional "edit" command, but also for resolving unexpected
1855 1855 conflicts).
1856 1856 """
1857 1857 # kludge: _chistedit only works for starting an edit, not aborting
1858 1858 # or continuing, so fall back to regular _texthistedit for those
1859 1859 # operations.
1860 1860 if (
1861 1861 ui.interface(b'histedit') == b'curses'
1862 1862 and _getgoal(pycompat.byteskwargs(opts)) == goalnew
1863 1863 ):
1864 1864 return _chistedit(ui, repo, *freeargs, **opts)
1865 1865 return _texthistedit(ui, repo, *freeargs, **opts)
1866 1866
1867 1867
1868 1868 def _texthistedit(ui, repo, *freeargs, **opts):
1869 1869 state = histeditstate(repo)
1870 1870 with repo.wlock() as wlock, repo.lock() as lock:
1871 1871 state.wlock = wlock
1872 1872 state.lock = lock
1873 1873 _histedit(ui, repo, state, *freeargs, **opts)
1874 1874
1875 1875
1876 1876 goalcontinue = b'continue'
1877 1877 goalabort = b'abort'
1878 1878 goaleditplan = b'edit-plan'
1879 1879 goalnew = b'new'
1880 1880
1881 1881
1882 1882 def _getgoal(opts):
1883 1883 if opts.get(b'continue'):
1884 1884 return goalcontinue
1885 1885 if opts.get(b'abort'):
1886 1886 return goalabort
1887 1887 if opts.get(b'edit_plan'):
1888 1888 return goaleditplan
1889 1889 return goalnew
1890 1890
1891 1891
1892 1892 def _readfile(ui, path):
1893 1893 if path == b'-':
1894 1894 with ui.timeblockedsection(b'histedit'):
1895 1895 return ui.fin.read()
1896 1896 else:
1897 1897 with open(path, b'rb') as f:
1898 1898 return f.read()
1899 1899
1900 1900
1901 1901 def _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs):
1902 1902 # TODO only abort if we try to histedit mq patches, not just
1903 1903 # blanket if mq patches are applied somewhere
1904 1904 mq = getattr(repo, 'mq', None)
1905 1905 if mq and mq.applied:
1906 1906 raise error.Abort(_(b'source has mq patches applied'))
1907 1907
1908 1908 # basic argument incompatibility processing
1909 1909 outg = opts.get(b'outgoing')
1910 1910 editplan = opts.get(b'edit_plan')
1911 1911 abort = opts.get(b'abort')
1912 1912 force = opts.get(b'force')
1913 1913 if force and not outg:
1914 1914 raise error.Abort(_(b'--force only allowed with --outgoing'))
1915 1915 if goal == b'continue':
1916 1916 if any((outg, abort, revs, freeargs, rules, editplan)):
1917 1917 raise error.Abort(_(b'no arguments allowed with --continue'))
1918 1918 elif goal == b'abort':
1919 1919 if any((outg, revs, freeargs, rules, editplan)):
1920 1920 raise error.Abort(_(b'no arguments allowed with --abort'))
1921 1921 elif goal == b'edit-plan':
1922 1922 if any((outg, revs, freeargs)):
1923 1923 raise error.Abort(
1924 1924 _(b'only --commands argument allowed with --edit-plan')
1925 1925 )
1926 1926 else:
1927 1927 if state.inprogress():
1928 1928 raise error.Abort(
1929 1929 _(
1930 1930 b'history edit already in progress, try '
1931 1931 b'--continue or --abort'
1932 1932 )
1933 1933 )
1934 1934 if outg:
1935 1935 if revs:
1936 1936 raise error.Abort(_(b'no revisions allowed with --outgoing'))
1937 1937 if len(freeargs) > 1:
1938 1938 raise error.Abort(
1939 1939 _(b'only one repo argument allowed with --outgoing')
1940 1940 )
1941 1941 else:
1942 1942 revs.extend(freeargs)
1943 1943 if len(revs) == 0:
1944 1944 defaultrev = destutil.desthistedit(ui, repo)
1945 1945 if defaultrev is not None:
1946 1946 revs.append(defaultrev)
1947 1947
1948 1948 if len(revs) != 1:
1949 1949 raise error.Abort(
1950 1950 _(b'histedit requires exactly one ancestor revision')
1951 1951 )
1952 1952
1953 1953
1954 1954 def _histedit(ui, repo, state, *freeargs, **opts):
1955 1955 opts = pycompat.byteskwargs(opts)
1956 1956 fm = ui.formatter(b'histedit', opts)
1957 1957 fm.startitem()
1958 1958 goal = _getgoal(opts)
1959 1959 revs = opts.get(b'rev', [])
1960 1960 nobackup = not ui.configbool(b'rewrite', b'backup-bundle')
1961 1961 rules = opts.get(b'commands', b'')
1962 1962 state.keep = opts.get(b'keep', False)
1963 1963
1964 1964 _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs)
1965 1965
1966 1966 hastags = False
1967 1967 if revs:
1968 1968 revs = scmutil.revrange(repo, revs)
1969 1969 ctxs = [repo[rev] for rev in revs]
1970 1970 for ctx in ctxs:
1971 1971 tags = [tag for tag in ctx.tags() if tag != b'tip']
1972 1972 if not hastags:
1973 1973 hastags = len(tags)
1974 1974 if hastags:
1975 1975 if ui.promptchoice(
1976 1976 _(
1977 1977 b'warning: tags associated with the given'
1978 1978 b' changeset will be lost after histedit.\n'
1979 1979 b'do you want to continue (yN)? $$ &Yes $$ &No'
1980 1980 ),
1981 1981 default=1,
1982 1982 ):
1983 1983 raise error.Abort(_(b'histedit cancelled\n'))
1984 1984 # rebuild state
1985 1985 if goal == goalcontinue:
1986 1986 state.read()
1987 1987 state = bootstrapcontinue(ui, state, opts)
1988 1988 elif goal == goaleditplan:
1989 1989 _edithisteditplan(ui, repo, state, rules)
1990 1990 return
1991 1991 elif goal == goalabort:
1992 1992 _aborthistedit(ui, repo, state, nobackup=nobackup)
1993 1993 return
1994 1994 else:
1995 1995 # goal == goalnew
1996 1996 _newhistedit(ui, repo, state, revs, freeargs, opts)
1997 1997
1998 1998 _continuehistedit(ui, repo, state)
1999 1999 _finishhistedit(ui, repo, state, fm)
2000 2000 fm.end()
2001 2001
2002 2002
2003 2003 def _continuehistedit(ui, repo, state):
2004 2004 """This function runs after either:
2005 2005 - bootstrapcontinue (if the goal is 'continue')
2006 2006 - _newhistedit (if the goal is 'new')
2007 2007 """
2008 2008 # preprocess rules so that we can hide inner folds from the user
2009 2009 # and only show one editor
2010 2010 actions = state.actions[:]
2011 2011 for idx, (action, nextact) in enumerate(zip(actions, actions[1:] + [None])):
2012 2012 if action.verb == b'fold' and nextact and nextact.verb == b'fold':
2013 2013 state.actions[idx].__class__ = _multifold
2014 2014
2015 2015 # Force an initial state file write, so the user can run --abort/continue
2016 2016 # even if there's an exception before the first transaction serialize.
2017 2017 state.write()
2018 2018
2019 2019 tr = None
2020 2020 # Don't use singletransaction by default since it rolls the entire
2021 2021 # transaction back if an unexpected exception happens (like a
2022 2022 # pretxncommit hook throws, or the user aborts the commit msg editor).
2023 2023 if ui.configbool(b"histedit", b"singletransaction"):
2024 2024 # Don't use a 'with' for the transaction, since actions may close
2025 2025 # and reopen a transaction. For example, if the action executes an
2026 2026 # external process it may choose to commit the transaction first.
2027 2027 tr = repo.transaction(b'histedit')
2028 2028 progress = ui.makeprogress(
2029 2029 _(b"editing"), unit=_(b'changes'), total=len(state.actions)
2030 2030 )
2031 2031 with progress, util.acceptintervention(tr):
2032 2032 while state.actions:
2033 2033 state.write(tr=tr)
2034 2034 actobj = state.actions[0]
2035 2035 progress.increment(item=actobj.torule())
2036 2036 ui.debug(
2037 2037 b'histedit: processing %s %s\n' % (actobj.verb, actobj.torule())
2038 2038 )
2039 2039 parentctx, replacement_ = actobj.run()
2040 2040 state.parentctxnode = parentctx.node()
2041 2041 state.replacements.extend(replacement_)
2042 2042 state.actions.pop(0)
2043 2043
2044 2044 state.write()
2045 2045
2046 2046
2047 2047 def _finishhistedit(ui, repo, state, fm):
2048 2048 """This action runs when histedit is finishing its session"""
2049 2049 hg.updaterepo(repo, state.parentctxnode, overwrite=False)
2050 2050
2051 2051 mapping, tmpnodes, created, ntm = processreplacement(state)
2052 2052 if mapping:
2053 2053 for prec, succs in pycompat.iteritems(mapping):
2054 2054 if not succs:
2055 2055 ui.debug(b'histedit: %s is dropped\n' % node.short(prec))
2056 2056 else:
2057 2057 ui.debug(
2058 2058 b'histedit: %s is replaced by %s\n'
2059 2059 % (node.short(prec), node.short(succs[0]))
2060 2060 )
2061 2061 if len(succs) > 1:
2062 2062 m = b'histedit: %s'
2063 2063 for n in succs[1:]:
2064 2064 ui.debug(m % node.short(n))
2065 2065
2066 2066 if not state.keep:
2067 2067 if mapping:
2068 2068 movetopmostbookmarks(repo, state.topmost, ntm)
2069 2069 # TODO update mq state
2070 2070 else:
2071 2071 mapping = {}
2072 2072
2073 2073 for n in tmpnodes:
2074 2074 if n in repo:
2075 2075 mapping[n] = ()
2076 2076
2077 2077 # remove entries about unknown nodes
2078 2078 has_node = repo.unfiltered().changelog.index.has_node
2079 2079 mapping = {
2080 2080 k: v
2081 2081 for k, v in mapping.items()
2082 2082 if has_node(k) and all(has_node(n) for n in v)
2083 2083 }
2084 2084 scmutil.cleanupnodes(repo, mapping, b'histedit')
2085 2085 hf = fm.hexfunc
2086 2086 fl = fm.formatlist
2087 2087 fd = fm.formatdict
2088 2088 nodechanges = fd(
2089 2089 {
2090 2090 hf(oldn): fl([hf(n) for n in newn], name=b'node')
2091 2091 for oldn, newn in pycompat.iteritems(mapping)
2092 2092 },
2093 2093 key=b"oldnode",
2094 2094 value=b"newnodes",
2095 2095 )
2096 2096 fm.data(nodechanges=nodechanges)
2097 2097
2098 2098 state.clear()
2099 2099 if os.path.exists(repo.sjoin(b'undo')):
2100 2100 os.unlink(repo.sjoin(b'undo'))
2101 2101 if repo.vfs.exists(b'histedit-last-edit.txt'):
2102 2102 repo.vfs.unlink(b'histedit-last-edit.txt')
2103 2103
2104 2104
2105 2105 def _aborthistedit(ui, repo, state, nobackup=False):
2106 2106 try:
2107 2107 state.read()
2108 2108 __, leafs, tmpnodes, __ = processreplacement(state)
2109 2109 ui.debug(b'restore wc to old parent %s\n' % node.short(state.topmost))
2110 2110
2111 2111 # Recover our old commits if necessary
2112 2112 if not state.topmost in repo and state.backupfile:
2113 2113 backupfile = repo.vfs.join(state.backupfile)
2114 2114 f = hg.openpath(ui, backupfile)
2115 2115 gen = exchange.readbundle(ui, f, backupfile)
2116 2116 with repo.transaction(b'histedit.abort') as tr:
2117 2117 bundle2.applybundle(
2118 2118 repo,
2119 2119 gen,
2120 2120 tr,
2121 2121 source=b'histedit',
2122 2122 url=b'bundle:' + backupfile,
2123 2123 )
2124 2124
2125 2125 os.remove(backupfile)
2126 2126
2127 2127 # check whether we should update away
2128 2128 if repo.unfiltered().revs(
2129 2129 b'parents() and (%n or %ln::)',
2130 2130 state.parentctxnode,
2131 2131 leafs | tmpnodes,
2132 2132 ):
2133 2133 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
2134 2134 cleanupnode(ui, repo, tmpnodes, nobackup=nobackup)
2135 2135 cleanupnode(ui, repo, leafs, nobackup=nobackup)
2136 2136 except Exception:
2137 2137 if state.inprogress():
2138 2138 ui.warn(
2139 2139 _(
2140 2140 b'warning: encountered an exception during histedit '
2141 2141 b'--abort; the repository may not have been completely '
2142 2142 b'cleaned up\n'
2143 2143 )
2144 2144 )
2145 2145 raise
2146 2146 finally:
2147 2147 state.clear()
2148 2148
2149 2149
2150 2150 def hgaborthistedit(ui, repo):
2151 2151 state = histeditstate(repo)
2152 2152 nobackup = not ui.configbool(b'rewrite', b'backup-bundle')
2153 2153 with repo.wlock() as wlock, repo.lock() as lock:
2154 2154 state.wlock = wlock
2155 2155 state.lock = lock
2156 2156 _aborthistedit(ui, repo, state, nobackup=nobackup)
2157 2157
2158 2158
2159 2159 def _edithisteditplan(ui, repo, state, rules):
2160 2160 state.read()
2161 2161 if not rules:
2162 2162 comment = geteditcomment(
2163 2163 ui, node.short(state.parentctxnode), node.short(state.topmost)
2164 2164 )
2165 2165 rules = ruleeditor(repo, ui, state.actions, comment)
2166 2166 else:
2167 2167 rules = _readfile(ui, rules)
2168 2168 actions = parserules(rules, state)
2169 2169 ctxs = [repo[act.node] for act in state.actions if act.node]
2170 2170 warnverifyactions(ui, repo, actions, state, ctxs)
2171 2171 state.actions = actions
2172 2172 state.write()
2173 2173
2174 2174
2175 2175 def _newhistedit(ui, repo, state, revs, freeargs, opts):
2176 2176 outg = opts.get(b'outgoing')
2177 2177 rules = opts.get(b'commands', b'')
2178 2178 force = opts.get(b'force')
2179 2179
2180 2180 cmdutil.checkunfinished(repo)
2181 2181 cmdutil.bailifchanged(repo)
2182 2182
2183 2183 topmost = repo.dirstate.p1()
2184 2184 if outg:
2185 2185 if freeargs:
2186 2186 remote = freeargs[0]
2187 2187 else:
2188 2188 remote = None
2189 2189 root = findoutgoing(ui, repo, remote, force, opts)
2190 2190 else:
2191 2191 rr = list(repo.set(b'roots(%ld)', scmutil.revrange(repo, revs)))
2192 2192 if len(rr) != 1:
2193 2193 raise error.Abort(
2194 2194 _(
2195 2195 b'The specified revisions must have '
2196 2196 b'exactly one common root'
2197 2197 )
2198 2198 )
2199 2199 root = rr[0].node()
2200 2200
2201 2201 revs = between(repo, root, topmost, state.keep)
2202 2202 if not revs:
2203 2203 raise error.Abort(
2204 2204 _(b'%s is not an ancestor of working directory') % node.short(root)
2205 2205 )
2206 2206
2207 2207 ctxs = [repo[r] for r in revs]
2208 2208
2209 2209 wctx = repo[None]
2210 2210 # Please don't ask me why `ancestors` is this value. I figured it
2211 2211 # out with print-debugging, not by actually understanding what the
2212 2212 # merge code is doing. :(
2213 2213 ancs = [repo[b'.']]
2214 2214 # Sniff-test to make sure we won't collide with untracked files in
2215 2215 # the working directory. If we don't do this, we can get a
2216 2216 # collision after we've started histedit and backing out gets ugly
2217 2217 # for everyone, especially the user.
2218 2218 for c in [ctxs[0].p1()] + ctxs:
2219 2219 try:
2220 2220 mergemod.calculateupdates(
2221 2221 repo,
2222 2222 wctx,
2223 2223 c,
2224 2224 ancs,
2225 2225 # These parameters were determined by print-debugging
2226 2226 # what happens later on inside histedit.
2227 2227 branchmerge=False,
2228 2228 force=False,
2229 2229 acceptremote=False,
2230 2230 followcopies=False,
2231 2231 )
2232 2232 except error.Abort:
2233 2233 raise error.Abort(
2234 2234 _(
2235 2235 b"untracked files in working directory conflict with files in %s"
2236 2236 )
2237 2237 % c
2238 2238 )
2239 2239
2240 2240 if not rules:
2241 2241 comment = geteditcomment(ui, node.short(root), node.short(topmost))
2242 2242 actions = [pick(state, r) for r in revs]
2243 2243 rules = ruleeditor(repo, ui, actions, comment)
2244 2244 else:
2245 2245 rules = _readfile(ui, rules)
2246 2246 actions = parserules(rules, state)
2247 2247 warnverifyactions(ui, repo, actions, state, ctxs)
2248 2248
2249 2249 parentctxnode = repo[root].p1().node()
2250 2250
2251 2251 state.parentctxnode = parentctxnode
2252 2252 state.actions = actions
2253 2253 state.topmost = topmost
2254 2254 state.replacements = []
2255 2255
2256 2256 ui.log(
2257 2257 b"histedit",
2258 2258 b"%d actions to histedit\n",
2259 2259 len(actions),
2260 2260 histedit_num_actions=len(actions),
2261 2261 )
2262 2262
2263 2263 # Create a backup so we can always abort completely.
2264 2264 backupfile = None
2265 2265 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
2266 2266 backupfile = repair.backupbundle(
2267 2267 repo, [parentctxnode], [topmost], root, b'histedit'
2268 2268 )
2269 2269 state.backupfile = backupfile
2270 2270
2271 2271
2272 2272 def _getsummary(ctx):
2273 2273 # a common pattern is to extract the summary but default to the empty
2274 2274 # string
2275 2275 summary = ctx.description() or b''
2276 2276 if summary:
2277 2277 summary = summary.splitlines()[0]
2278 2278 return summary
2279 2279
2280 2280
2281 2281 def bootstrapcontinue(ui, state, opts):
2282 2282 repo = state.repo
2283 2283
2284 2284 ms = mergemod.mergestate.read(repo)
2285 2285 mergeutil.checkunresolved(ms)
2286 2286
2287 2287 if state.actions:
2288 2288 actobj = state.actions.pop(0)
2289 2289
2290 2290 if _isdirtywc(repo):
2291 2291 actobj.continuedirty()
2292 2292 if _isdirtywc(repo):
2293 2293 abortdirty()
2294 2294
2295 2295 parentctx, replacements = actobj.continueclean()
2296 2296
2297 2297 state.parentctxnode = parentctx.node()
2298 2298 state.replacements.extend(replacements)
2299 2299
2300 2300 return state
2301 2301
2302 2302
2303 2303 def between(repo, old, new, keep):
2304 2304 """select and validate the set of revision to edit
2305 2305
2306 2306 When keep is false, the specified set can't have children."""
2307 2307 revs = repo.revs(b'%n::%n', old, new)
2308 2308 if revs and not keep:
2309 2309 if not obsolete.isenabled(
2310 2310 repo, obsolete.allowunstableopt
2311 2311 ) and repo.revs(b'(%ld::) - (%ld)', revs, revs):
2312 2312 raise error.Abort(
2313 2313 _(
2314 2314 b'can only histedit a changeset together '
2315 2315 b'with all its descendants'
2316 2316 )
2317 2317 )
2318 2318 if repo.revs(b'(%ld) and merge()', revs):
2319 2319 raise error.Abort(_(b'cannot edit history that contains merges'))
2320 2320 root = repo[revs.first()] # list is already sorted by repo.revs()
2321 2321 if not root.mutable():
2322 2322 raise error.Abort(
2323 2323 _(b'cannot edit public changeset: %s') % root,
2324 2324 hint=_(b"see 'hg help phases' for details"),
2325 2325 )
2326 2326 return pycompat.maplist(repo.changelog.node, revs)
2327 2327
2328 2328
2329 2329 def ruleeditor(repo, ui, actions, editcomment=b""):
2330 2330 """open an editor to edit rules
2331 2331
2332 2332 rules are in the format [ [act, ctx], ...] like in state.rules
2333 2333 """
2334 2334 if repo.ui.configbool(b"experimental", b"histedit.autoverb"):
2335 2335 newact = util.sortdict()
2336 2336 for act in actions:
2337 2337 ctx = repo[act.node]
2338 2338 summary = _getsummary(ctx)
2339 2339 fword = summary.split(b' ', 1)[0].lower()
2340 2340 added = False
2341 2341
2342 2342 # if it doesn't end with the special character '!' just skip this
2343 2343 if fword.endswith(b'!'):
2344 2344 fword = fword[:-1]
2345 2345 if fword in primaryactions | secondaryactions | tertiaryactions:
2346 2346 act.verb = fword
2347 2347 # get the target summary
2348 2348 tsum = summary[len(fword) + 1 :].lstrip()
2349 2349 # safe but slow: reverse iterate over the actions so we
2350 2350 # don't clash on two commits having the same summary
2351 2351 for na, l in reversed(list(pycompat.iteritems(newact))):
2352 2352 actx = repo[na.node]
2353 2353 asum = _getsummary(actx)
2354 2354 if asum == tsum:
2355 2355 added = True
2356 2356 l.append(act)
2357 2357 break
2358 2358
2359 2359 if not added:
2360 2360 newact[act] = []
2361 2361
2362 2362 # copy over and flatten the new list
2363 2363 actions = []
2364 2364 for na, l in pycompat.iteritems(newact):
2365 2365 actions.append(na)
2366 2366 actions += l
2367 2367
2368 2368 rules = b'\n'.join([act.torule() for act in actions])
2369 2369 rules += b'\n\n'
2370 2370 rules += editcomment
2371 2371 rules = ui.edit(
2372 2372 rules,
2373 2373 ui.username(),
2374 2374 {b'prefix': b'histedit'},
2375 2375 repopath=repo.path,
2376 2376 action=b'histedit',
2377 2377 )
2378 2378
2379 2379 # Save edit rules in .hg/histedit-last-edit.txt in case
2380 2380 # the user needs to ask for help after something
2381 2381 # surprising happens.
2382 2382 with repo.vfs(b'histedit-last-edit.txt', b'wb') as f:
2383 2383 f.write(rules)
2384 2384
2385 2385 return rules
2386 2386
2387 2387
2388 2388 def parserules(rules, state):
2389 2389 """Read the histedit rules string and return list of action objects """
2390 2390 rules = [
2391 2391 l
2392 2392 for l in (r.strip() for r in rules.splitlines())
2393 2393 if l and not l.startswith(b'#')
2394 2394 ]
2395 2395 actions = []
2396 2396 for r in rules:
2397 2397 if b' ' not in r:
2398 2398 raise error.ParseError(_(b'malformed line "%s"') % r)
2399 2399 verb, rest = r.split(b' ', 1)
2400 2400
2401 2401 if verb not in actiontable:
2402 2402 raise error.ParseError(_(b'unknown action "%s"') % verb)
2403 2403
2404 2404 action = actiontable[verb].fromrule(state, rest)
2405 2405 actions.append(action)
2406 2406 return actions
2407 2407
2408 2408
2409 2409 def warnverifyactions(ui, repo, actions, state, ctxs):
2410 2410 try:
2411 2411 verifyactions(actions, state, ctxs)
2412 2412 except error.ParseError:
2413 2413 if repo.vfs.exists(b'histedit-last-edit.txt'):
2414 2414 ui.warn(
2415 2415 _(
2416 2416 b'warning: histedit rules saved '
2417 2417 b'to: .hg/histedit-last-edit.txt\n'
2418 2418 )
2419 2419 )
2420 2420 raise
2421 2421
2422 2422
2423 2423 def verifyactions(actions, state, ctxs):
2424 2424 """Verify that there exists exactly one action per given changeset and
2425 2425 other constraints.
2426 2426
2427 2427 Will abort if there are to many or too few rules, a malformed rule,
2428 2428 or a rule on a changeset outside of the user-given range.
2429 2429 """
2430 2430 expected = set(c.node() for c in ctxs)
2431 2431 seen = set()
2432 2432 prev = None
2433 2433
2434 2434 if actions and actions[0].verb in [b'roll', b'fold']:
2435 2435 raise error.ParseError(
2436 2436 _(b'first changeset cannot use verb "%s"') % actions[0].verb
2437 2437 )
2438 2438
2439 2439 for action in actions:
2440 2440 action.verify(prev, expected, seen)
2441 2441 prev = action
2442 2442 if action.node is not None:
2443 2443 seen.add(action.node)
2444 2444 missing = sorted(expected - seen) # sort to stabilize output
2445 2445
2446 2446 if state.repo.ui.configbool(b'histedit', b'dropmissing'):
2447 2447 if len(actions) == 0:
2448 2448 raise error.ParseError(
2449 2449 _(b'no rules provided'),
2450 2450 hint=_(b'use strip extension to remove commits'),
2451 2451 )
2452 2452
2453 2453 drops = [drop(state, n) for n in missing]
2454 2454 # put the in the beginning so they execute immediately and
2455 2455 # don't show in the edit-plan in the future
2456 2456 actions[:0] = drops
2457 2457 elif missing:
2458 2458 raise error.ParseError(
2459 2459 _(b'missing rules for changeset %s') % node.short(missing[0]),
2460 2460 hint=_(
2461 2461 b'use "drop %s" to discard, see also: '
2462 2462 b"'hg help -e histedit.config'"
2463 2463 )
2464 2464 % node.short(missing[0]),
2465 2465 )
2466 2466
2467 2467
2468 2468 def adjustreplacementsfrommarkers(repo, oldreplacements):
2469 2469 """Adjust replacements from obsolescence markers
2470 2470
2471 2471 Replacements structure is originally generated based on
2472 2472 histedit's state and does not account for changes that are
2473 2473 not recorded there. This function fixes that by adding
2474 2474 data read from obsolescence markers"""
2475 2475 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
2476 2476 return oldreplacements
2477 2477
2478 2478 unfi = repo.unfiltered()
2479 2479 get_rev = unfi.changelog.index.get_rev
2480 2480 obsstore = repo.obsstore
2481 2481 newreplacements = list(oldreplacements)
2482 2482 oldsuccs = [r[1] for r in oldreplacements]
2483 2483 # successors that have already been added to succstocheck once
2484 2484 seensuccs = set().union(
2485 2485 *oldsuccs
2486 2486 ) # create a set from an iterable of tuples
2487 2487 succstocheck = list(seensuccs)
2488 2488 while succstocheck:
2489 2489 n = succstocheck.pop()
2490 2490 missing = get_rev(n) is None
2491 2491 markers = obsstore.successors.get(n, ())
2492 2492 if missing and not markers:
2493 2493 # dead end, mark it as such
2494 2494 newreplacements.append((n, ()))
2495 2495 for marker in markers:
2496 2496 nsuccs = marker[1]
2497 2497 newreplacements.append((n, nsuccs))
2498 2498 for nsucc in nsuccs:
2499 2499 if nsucc not in seensuccs:
2500 2500 seensuccs.add(nsucc)
2501 2501 succstocheck.append(nsucc)
2502 2502
2503 2503 return newreplacements
2504 2504
2505 2505
2506 2506 def processreplacement(state):
2507 2507 """process the list of replacements to return
2508 2508
2509 2509 1) the final mapping between original and created nodes
2510 2510 2) the list of temporary node created by histedit
2511 2511 3) the list of new commit created by histedit"""
2512 2512 replacements = adjustreplacementsfrommarkers(state.repo, state.replacements)
2513 2513 allsuccs = set()
2514 2514 replaced = set()
2515 2515 fullmapping = {}
2516 2516 # initialize basic set
2517 2517 # fullmapping records all operations recorded in replacement
2518 2518 for rep in replacements:
2519 2519 allsuccs.update(rep[1])
2520 2520 replaced.add(rep[0])
2521 2521 fullmapping.setdefault(rep[0], set()).update(rep[1])
2522 2522 new = allsuccs - replaced
2523 2523 tmpnodes = allsuccs & replaced
2524 2524 # Reduce content fullmapping into direct relation between original nodes
2525 2525 # and final node created during history edition
2526 2526 # Dropped changeset are replaced by an empty list
2527 2527 toproceed = set(fullmapping)
2528 2528 final = {}
2529 2529 while toproceed:
2530 2530 for x in list(toproceed):
2531 2531 succs = fullmapping[x]
2532 2532 for s in list(succs):
2533 2533 if s in toproceed:
2534 2534 # non final node with unknown closure
2535 2535 # We can't process this now
2536 2536 break
2537 2537 elif s in final:
2538 2538 # non final node, replace with closure
2539 2539 succs.remove(s)
2540 2540 succs.update(final[s])
2541 2541 else:
2542 2542 final[x] = succs
2543 2543 toproceed.remove(x)
2544 2544 # remove tmpnodes from final mapping
2545 2545 for n in tmpnodes:
2546 2546 del final[n]
2547 2547 # we expect all changes involved in final to exist in the repo
2548 2548 # turn `final` into list (topologically sorted)
2549 2549 get_rev = state.repo.changelog.index.get_rev
2550 2550 for prec, succs in final.items():
2551 2551 final[prec] = sorted(succs, key=get_rev)
2552 2552
2553 2553 # computed topmost element (necessary for bookmark)
2554 2554 if new:
2555 2555 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
2556 2556 elif not final:
2557 2557 # Nothing rewritten at all. we won't need `newtopmost`
2558 2558 # It is the same as `oldtopmost` and `processreplacement` know it
2559 2559 newtopmost = None
2560 2560 else:
2561 2561 # every body died. The newtopmost is the parent of the root.
2562 2562 r = state.repo.changelog.rev
2563 2563 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
2564 2564
2565 2565 return final, tmpnodes, new, newtopmost
2566 2566
2567 2567
2568 2568 def movetopmostbookmarks(repo, oldtopmost, newtopmost):
2569 2569 """Move bookmark from oldtopmost to newly created topmost
2570 2570
2571 2571 This is arguably a feature and we may only want that for the active
2572 2572 bookmark. But the behavior is kept compatible with the old version for now.
2573 2573 """
2574 2574 if not oldtopmost or not newtopmost:
2575 2575 return
2576 2576 oldbmarks = repo.nodebookmarks(oldtopmost)
2577 2577 if oldbmarks:
2578 2578 with repo.lock(), repo.transaction(b'histedit') as tr:
2579 2579 marks = repo._bookmarks
2580 2580 changes = []
2581 2581 for name in oldbmarks:
2582 2582 changes.append((name, newtopmost))
2583 2583 marks.applychanges(repo, tr, changes)
2584 2584
2585 2585
2586 2586 def cleanupnode(ui, repo, nodes, nobackup=False):
2587 2587 """strip a group of nodes from the repository
2588 2588
2589 2589 The set of node to strip may contains unknown nodes."""
2590 2590 with repo.lock():
2591 2591 # do not let filtering get in the way of the cleanse
2592 2592 # we should probably get rid of obsolescence marker created during the
2593 2593 # histedit, but we currently do not have such information.
2594 2594 repo = repo.unfiltered()
2595 2595 # Find all nodes that need to be stripped
2596 2596 # (we use %lr instead of %ln to silently ignore unknown items)
2597 2597 has_node = repo.changelog.index.has_node
2598 2598 nodes = sorted(n for n in nodes if has_node(n))
2599 2599 roots = [c.node() for c in repo.set(b"roots(%ln)", nodes)]
2600 2600 if roots:
2601 2601 backup = not nobackup
2602 2602 repair.strip(ui, repo, roots, backup=backup)
2603 2603
2604 2604
2605 2605 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
2606 if isinstance(nodelist, str):
2606 if isinstance(nodelist, bytes):
2607 2607 nodelist = [nodelist]
2608 2608 state = histeditstate(repo)
2609 2609 if state.inprogress():
2610 2610 state.read()
2611 2611 histedit_nodes = {
2612 2612 action.node for action in state.actions if action.node
2613 2613 }
2614 2614 common_nodes = histedit_nodes & set(nodelist)
2615 2615 if common_nodes:
2616 2616 raise error.Abort(
2617 2617 _(b"histedit in progress, can't strip %s")
2618 2618 % b', '.join(node.short(x) for x in common_nodes)
2619 2619 )
2620 2620 return orig(ui, repo, nodelist, *args, **kwargs)
2621 2621
2622 2622
2623 2623 extensions.wrapfunction(repair, b'strip', stripwrapper)
2624 2624
2625 2625
2626 2626 def summaryhook(ui, repo):
2627 2627 state = histeditstate(repo)
2628 2628 if not state.inprogress():
2629 2629 return
2630 2630 state.read()
2631 2631 if state.actions:
2632 2632 # i18n: column positioning for "hg summary"
2633 2633 ui.write(
2634 2634 _(b'hist: %s (histedit --continue)\n')
2635 2635 % (
2636 2636 ui.label(_(b'%d remaining'), b'histedit.remaining')
2637 2637 % len(state.actions)
2638 2638 )
2639 2639 )
2640 2640
2641 2641
2642 2642 def extsetup(ui):
2643 2643 cmdutil.summaryhooks.add(b'histedit', summaryhook)
2644 2644 statemod.addunfinished(
2645 2645 b'histedit',
2646 2646 fname=b'histedit-state',
2647 2647 allowcommit=True,
2648 2648 continueflag=True,
2649 2649 abortfunc=hgaborthistedit,
2650 2650 )
@@ -1,221 +1,221 b''
1 1 # win32mbcs.py -- MBCS filename support for Mercurial
2 2 #
3 3 # Copyright (c) 2008 Shun-ichi Goto <shunichi.goto@gmail.com>
4 4 #
5 5 # Version: 0.3
6 6 # Author: Shun-ichi Goto <shunichi.goto@gmail.com>
7 7 #
8 8 # This software may be used and distributed according to the terms of the
9 9 # GNU General Public License version 2 or any later version.
10 10 #
11 11
12 12 '''allow the use of MBCS paths with problematic encodings
13 13
14 14 Some MBCS encodings are not good for some path operations (i.e.
15 15 splitting path, case conversion, etc.) with its encoded bytes. We call
16 16 such a encoding (i.e. shift_jis and big5) as "problematic encoding".
17 17 This extension can be used to fix the issue with those encodings by
18 18 wrapping some functions to convert to Unicode string before path
19 19 operation.
20 20
21 21 This extension is useful for:
22 22
23 23 - Japanese Windows users using shift_jis encoding.
24 24 - Chinese Windows users using big5 encoding.
25 25 - All users who use a repository with one of problematic encodings on
26 26 case-insensitive file system.
27 27
28 28 This extension is not needed for:
29 29
30 30 - Any user who use only ASCII chars in path.
31 31 - Any user who do not use any of problematic encodings.
32 32
33 33 Note that there are some limitations on using this extension:
34 34
35 35 - You should use single encoding in one repository.
36 36 - If the repository path ends with 0x5c, .hg/hgrc cannot be read.
37 37 - win32mbcs is not compatible with fixutf8 extension.
38 38
39 39 By default, win32mbcs uses encoding.encoding decided by Mercurial.
40 40 You can specify the encoding by config option::
41 41
42 42 [win32mbcs]
43 43 encoding = sjis
44 44
45 45 It is useful for the users who want to commit with UTF-8 log message.
46 46 '''
47 47 from __future__ import absolute_import
48 48
49 49 import os
50 50 import sys
51 51
52 52 from mercurial.i18n import _
53 53 from mercurial.pycompat import getattr, setattr
54 54 from mercurial import (
55 55 encoding,
56 56 error,
57 57 pycompat,
58 58 registrar,
59 59 )
60 60
61 61 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
62 62 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
63 63 # be specifying the version(s) of Mercurial they are tested with, or
64 64 # leave the attribute unspecified.
65 65 testedwith = b'ships-with-hg-core'
66 66
67 67 configtable = {}
68 68 configitem = registrar.configitem(configtable)
69 69
70 70 # Encoding.encoding may be updated by --encoding option.
71 71 # Use a lambda do delay the resolution.
72 72 configitem(
73 73 b'win32mbcs', b'encoding', default=lambda: encoding.encoding,
74 74 )
75 75
76 76 _encoding = None # see extsetup
77 77
78 78
79 79 def decode(arg):
80 if isinstance(arg, str):
80 if isinstance(arg, bytes):
81 81 uarg = arg.decode(_encoding)
82 82 if arg == uarg.encode(_encoding):
83 83 return uarg
84 84 raise UnicodeError(b"Not local encoding")
85 85 elif isinstance(arg, tuple):
86 86 return tuple(map(decode, arg))
87 87 elif isinstance(arg, list):
88 88 return map(decode, arg)
89 89 elif isinstance(arg, dict):
90 90 for k, v in arg.items():
91 91 arg[k] = decode(v)
92 92 return arg
93 93
94 94
95 95 def encode(arg):
96 96 if isinstance(arg, pycompat.unicode):
97 97 return arg.encode(_encoding)
98 98 elif isinstance(arg, tuple):
99 99 return tuple(map(encode, arg))
100 100 elif isinstance(arg, list):
101 101 return map(encode, arg)
102 102 elif isinstance(arg, dict):
103 103 for k, v in arg.items():
104 104 arg[k] = encode(v)
105 105 return arg
106 106
107 107
108 108 def appendsep(s):
109 109 # ensure the path ends with os.sep, appending it if necessary.
110 110 try:
111 111 us = decode(s)
112 112 except UnicodeError:
113 113 us = s
114 114 if us and us[-1] not in b':/\\':
115 115 s += pycompat.ossep
116 116 return s
117 117
118 118
119 119 def basewrapper(func, argtype, enc, dec, args, kwds):
120 120 # check check already converted, then call original
121 121 for arg in args:
122 122 if isinstance(arg, argtype):
123 123 return func(*args, **kwds)
124 124
125 125 try:
126 126 # convert string arguments, call func, then convert back the
127 127 # return value.
128 128 return enc(func(*dec(args), **dec(kwds)))
129 129 except UnicodeError:
130 130 raise error.Abort(
131 131 _(b"[win32mbcs] filename conversion failed with %s encoding\n")
132 132 % _encoding
133 133 )
134 134
135 135
136 136 def wrapper(func, args, kwds):
137 137 return basewrapper(func, pycompat.unicode, encode, decode, args, kwds)
138 138
139 139
140 140 def reversewrapper(func, args, kwds):
141 141 return basewrapper(func, str, decode, encode, args, kwds)
142 142
143 143
144 144 def wrapperforlistdir(func, args, kwds):
145 145 # Ensure 'path' argument ends with os.sep to avoids
146 146 # misinterpreting last 0x5c of MBCS 2nd byte as path separator.
147 147 if args:
148 148 args = list(args)
149 149 args[0] = appendsep(args[0])
150 150 if b'path' in kwds:
151 151 kwds[b'path'] = appendsep(kwds[b'path'])
152 152 return func(*args, **kwds)
153 153
154 154
155 155 def wrapname(name, wrapper):
156 156 module, name = name.rsplit(b'.', 1)
157 157 module = sys.modules[module]
158 158 func = getattr(module, name)
159 159
160 160 def f(*args, **kwds):
161 161 return wrapper(func, args, kwds)
162 162
163 163 f.__name__ = func.__name__
164 164 setattr(module, name, f)
165 165
166 166
167 167 # List of functions to be wrapped.
168 168 # NOTE: os.path.dirname() and os.path.basename() are safe because
169 169 # they use result of os.path.split()
170 170 funcs = b'''os.path.join os.path.split os.path.splitext
171 171 os.path.normpath os.makedirs mercurial.util.endswithsep
172 172 mercurial.util.splitpath mercurial.util.fscasesensitive
173 173 mercurial.util.fspath mercurial.util.pconvert mercurial.util.normpath
174 174 mercurial.util.checkwinfilename mercurial.util.checkosfilename
175 175 mercurial.util.split'''
176 176
177 177 # These functions are required to be called with local encoded string
178 178 # because they expects argument is local encoded string and cause
179 179 # problem with unicode string.
180 180 rfuncs = b'''mercurial.encoding.upper mercurial.encoding.lower
181 181 mercurial.util._filenamebytestr'''
182 182
183 183 # List of Windows specific functions to be wrapped.
184 184 winfuncs = b'''os.path.splitunc'''
185 185
186 186 # codec and alias names of sjis and big5 to be faked.
187 187 problematic_encodings = b'''big5 big5-tw csbig5 big5hkscs big5-hkscs
188 188 hkscs cp932 932 ms932 mskanji ms-kanji shift_jis csshiftjis shiftjis
189 189 sjis s_jis shift_jis_2004 shiftjis2004 sjis_2004 sjis2004
190 190 shift_jisx0213 shiftjisx0213 sjisx0213 s_jisx0213 950 cp950 ms950 '''
191 191
192 192
193 193 def extsetup(ui):
194 194 # TODO: decide use of config section for this extension
195 195 if (not os.path.supports_unicode_filenames) and (
196 196 pycompat.sysplatform != b'cygwin'
197 197 ):
198 198 ui.warn(_(b"[win32mbcs] cannot activate on this platform.\n"))
199 199 return
200 200 # determine encoding for filename
201 201 global _encoding
202 202 _encoding = ui.config(b'win32mbcs', b'encoding')
203 203 # fake is only for relevant environment.
204 204 if _encoding.lower() in problematic_encodings.split():
205 205 for f in funcs.split():
206 206 wrapname(f, wrapper)
207 207 if pycompat.iswindows:
208 208 for f in winfuncs.split():
209 209 wrapname(f, wrapper)
210 210 wrapname(b"mercurial.util.listdir", wrapperforlistdir)
211 211 wrapname(b"mercurial.windows.listdir", wrapperforlistdir)
212 212 # wrap functions to be called with local byte string arguments
213 213 for f in rfuncs.split():
214 214 wrapname(f, reversewrapper)
215 215 # Check sys.args manually instead of using ui.debug() because
216 216 # command line options is not yet applied when
217 217 # extensions.loadall() is called.
218 218 if b'--debug' in sys.argv:
219 219 ui.writenoi18n(
220 220 b"[win32mbcs] activated with encoding: %s\n" % _encoding
221 221 )
@@ -1,287 +1,287 b''
1 1 # hgweb/common.py - Utility functions needed by hgweb_mod and hgwebdir_mod
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from __future__ import absolute_import
10 10
11 11 import base64
12 12 import errno
13 13 import mimetypes
14 14 import os
15 15 import stat
16 16
17 17 from ..pycompat import (
18 18 getattr,
19 19 open,
20 20 )
21 21 from .. import (
22 22 encoding,
23 23 pycompat,
24 24 util,
25 25 )
26 26
27 27 httpserver = util.httpserver
28 28
29 29 HTTP_OK = 200
30 30 HTTP_CREATED = 201
31 31 HTTP_NOT_MODIFIED = 304
32 32 HTTP_BAD_REQUEST = 400
33 33 HTTP_UNAUTHORIZED = 401
34 34 HTTP_FORBIDDEN = 403
35 35 HTTP_NOT_FOUND = 404
36 36 HTTP_METHOD_NOT_ALLOWED = 405
37 37 HTTP_NOT_ACCEPTABLE = 406
38 38 HTTP_UNSUPPORTED_MEDIA_TYPE = 415
39 39 HTTP_SERVER_ERROR = 500
40 40
41 41
42 42 def ismember(ui, username, userlist):
43 43 """Check if username is a member of userlist.
44 44
45 45 If userlist has a single '*' member, all users are considered members.
46 46 Can be overridden by extensions to provide more complex authorization
47 47 schemes.
48 48 """
49 49 return userlist == [b'*'] or username in userlist
50 50
51 51
52 52 def checkauthz(hgweb, req, op):
53 53 '''Check permission for operation based on request data (including
54 54 authentication info). Return if op allowed, else raise an ErrorResponse
55 55 exception.'''
56 56
57 57 user = req.remoteuser
58 58
59 59 deny_read = hgweb.configlist(b'web', b'deny_read')
60 60 if deny_read and (not user or ismember(hgweb.repo.ui, user, deny_read)):
61 61 raise ErrorResponse(HTTP_UNAUTHORIZED, b'read not authorized')
62 62
63 63 allow_read = hgweb.configlist(b'web', b'allow_read')
64 64 if allow_read and (not ismember(hgweb.repo.ui, user, allow_read)):
65 65 raise ErrorResponse(HTTP_UNAUTHORIZED, b'read not authorized')
66 66
67 67 if op == b'pull' and not hgweb.allowpull:
68 68 raise ErrorResponse(HTTP_UNAUTHORIZED, b'pull not authorized')
69 69 elif op == b'pull' or op is None: # op is None for interface requests
70 70 return
71 71
72 72 # Allow LFS uploading via PUT requests
73 73 if op == b'upload':
74 74 if req.method != b'PUT':
75 75 msg = b'upload requires PUT request'
76 76 raise ErrorResponse(HTTP_METHOD_NOT_ALLOWED, msg)
77 77 # enforce that you can only push using POST requests
78 78 elif req.method != b'POST':
79 79 msg = b'push requires POST request'
80 80 raise ErrorResponse(HTTP_METHOD_NOT_ALLOWED, msg)
81 81
82 82 # require ssl by default for pushing, auth info cannot be sniffed
83 83 # and replayed
84 84 if hgweb.configbool(b'web', b'push_ssl') and req.urlscheme != b'https':
85 85 raise ErrorResponse(HTTP_FORBIDDEN, b'ssl required')
86 86
87 87 deny = hgweb.configlist(b'web', b'deny_push')
88 88 if deny and (not user or ismember(hgweb.repo.ui, user, deny)):
89 89 raise ErrorResponse(HTTP_UNAUTHORIZED, b'push not authorized')
90 90
91 91 allow = hgweb.configlist(b'web', b'allow-push')
92 92 if not (allow and ismember(hgweb.repo.ui, user, allow)):
93 93 raise ErrorResponse(HTTP_UNAUTHORIZED, b'push not authorized')
94 94
95 95
96 96 # Hooks for hgweb permission checks; extensions can add hooks here.
97 97 # Each hook is invoked like this: hook(hgweb, request, operation),
98 98 # where operation is either read, pull, push or upload. Hooks should either
99 99 # raise an ErrorResponse exception, or just return.
100 100 #
101 101 # It is possible to do both authentication and authorization through
102 102 # this.
103 103 permhooks = [checkauthz]
104 104
105 105
106 106 class ErrorResponse(Exception):
107 107 def __init__(self, code, message=None, headers=None):
108 108 if message is None:
109 109 message = _statusmessage(code)
110 110 Exception.__init__(self, pycompat.sysstr(message))
111 111 self.code = code
112 112 if headers is None:
113 113 headers = []
114 114 self.headers = headers
115 115 self.message = message
116 116
117 117
118 118 class continuereader(object):
119 119 """File object wrapper to handle HTTP 100-continue.
120 120
121 121 This is used by servers so they automatically handle Expect: 100-continue
122 122 request headers. On first read of the request body, the 100 Continue
123 123 response is sent. This should trigger the client into actually sending
124 124 the request body.
125 125 """
126 126
127 127 def __init__(self, f, write):
128 128 self.f = f
129 129 self._write = write
130 130 self.continued = False
131 131
132 132 def read(self, amt=-1):
133 133 if not self.continued:
134 134 self.continued = True
135 135 self._write(b'HTTP/1.1 100 Continue\r\n\r\n')
136 136 return self.f.read(amt)
137 137
138 138 def __getattr__(self, attr):
139 139 if attr in (b'close', b'readline', b'readlines', b'__iter__'):
140 140 return getattr(self.f, attr)
141 141 raise AttributeError
142 142
143 143
144 144 def _statusmessage(code):
145 145 responses = httpserver.basehttprequesthandler.responses
146 146 return pycompat.bytesurl(responses.get(code, ('Error', 'Unknown error'))[0])
147 147
148 148
149 149 def statusmessage(code, message=None):
150 150 return b'%d %s' % (code, message or _statusmessage(code))
151 151
152 152
153 153 def get_stat(spath, fn):
154 154 """stat fn if it exists, spath otherwise"""
155 155 cl_path = os.path.join(spath, fn)
156 156 if os.path.exists(cl_path):
157 157 return os.stat(cl_path)
158 158 else:
159 159 return os.stat(spath)
160 160
161 161
162 162 def get_mtime(spath):
163 163 return get_stat(spath, b"00changelog.i")[stat.ST_MTIME]
164 164
165 165
166 166 def ispathsafe(path):
167 167 """Determine if a path is safe to use for filesystem access."""
168 168 parts = path.split(b'/')
169 169 for part in parts:
170 170 if (
171 171 part in (b'', pycompat.oscurdir, pycompat.ospardir)
172 172 or pycompat.ossep in part
173 173 or pycompat.osaltsep is not None
174 174 and pycompat.osaltsep in part
175 175 ):
176 176 return False
177 177
178 178 return True
179 179
180 180
181 181 def staticfile(directory, fname, res):
182 182 """return a file inside directory with guessed Content-Type header
183 183
184 184 fname always uses '/' as directory separator and isn't allowed to
185 185 contain unusual path components.
186 186 Content-Type is guessed using the mimetypes module.
187 187 Return an empty string if fname is illegal or file not found.
188 188
189 189 """
190 190 if not ispathsafe(fname):
191 191 return
192 192
193 193 fpath = os.path.join(*fname.split(b'/'))
194 if isinstance(directory, str):
194 if isinstance(directory, bytes):
195 195 directory = [directory]
196 196 for d in directory:
197 197 path = os.path.join(d, fpath)
198 198 if os.path.exists(path):
199 199 break
200 200 try:
201 201 os.stat(path)
202 202 ct = pycompat.sysbytes(
203 203 mimetypes.guess_type(pycompat.fsdecode(path))[0] or r"text/plain"
204 204 )
205 205 with open(path, b'rb') as fh:
206 206 data = fh.read()
207 207
208 208 res.headers[b'Content-Type'] = ct
209 209 res.setbodybytes(data)
210 210 return res
211 211 except TypeError:
212 212 raise ErrorResponse(HTTP_SERVER_ERROR, b'illegal filename')
213 213 except OSError as err:
214 214 if err.errno == errno.ENOENT:
215 215 raise ErrorResponse(HTTP_NOT_FOUND)
216 216 else:
217 217 raise ErrorResponse(
218 218 HTTP_SERVER_ERROR, encoding.strtolocal(err.strerror)
219 219 )
220 220
221 221
222 222 def paritygen(stripecount, offset=0):
223 223 """count parity of horizontal stripes for easier reading"""
224 224 if stripecount and offset:
225 225 # account for offset, e.g. due to building the list in reverse
226 226 count = (stripecount + offset) % stripecount
227 227 parity = (stripecount + offset) // stripecount & 1
228 228 else:
229 229 count = 0
230 230 parity = 0
231 231 while True:
232 232 yield parity
233 233 count += 1
234 234 if stripecount and count >= stripecount:
235 235 parity = 1 - parity
236 236 count = 0
237 237
238 238
239 239 def get_contact(config):
240 240 """Return repo contact information or empty string.
241 241
242 242 web.contact is the primary source, but if that is not set, try
243 243 ui.username or $EMAIL as a fallback to display something useful.
244 244 """
245 245 return (
246 246 config(b"web", b"contact")
247 247 or config(b"ui", b"username")
248 248 or encoding.environ.get(b"EMAIL")
249 249 or b""
250 250 )
251 251
252 252
253 253 def cspvalues(ui):
254 254 """Obtain the Content-Security-Policy header and nonce value.
255 255
256 256 Returns a 2-tuple of the CSP header value and the nonce value.
257 257
258 258 First value is ``None`` if CSP isn't enabled. Second value is ``None``
259 259 if CSP isn't enabled or if the CSP header doesn't need a nonce.
260 260 """
261 261 # Without demandimport, "import uuid" could have an immediate side-effect
262 262 # running "ldconfig" on Linux trying to find libuuid.
263 263 # With Python <= 2.7.12, that "ldconfig" is run via a shell and the shell
264 264 # may pollute the terminal with:
265 265 #
266 266 # shell-init: error retrieving current directory: getcwd: cannot access
267 267 # parent directories: No such file or directory
268 268 #
269 269 # Python >= 2.7.13 has fixed it by running "ldconfig" directly without a
270 270 # shell (hg changeset a09ae70f3489).
271 271 #
272 272 # Moved "import uuid" from here so it's executed after we know we have
273 273 # a sane cwd (i.e. after dispatch.py cwd check).
274 274 #
275 275 # We can move it back once we no longer need Python <= 2.7.12 support.
276 276 import uuid
277 277
278 278 # Don't allow untrusted CSP setting since it be disable protections
279 279 # from a trusted/global source.
280 280 csp = ui.config(b'web', b'csp', untrusted=False)
281 281 nonce = None
282 282
283 283 if csp and b'%nonce%' in csp:
284 284 nonce = base64.urlsafe_b64encode(uuid.uuid4().bytes).rstrip(b'=')
285 285 csp = csp.replace(b'%nonce%', nonce)
286 286
287 287 return csp, nonce
@@ -1,579 +1,579 b''
1 1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from __future__ import absolute_import
10 10
11 11 import gc
12 12 import os
13 13 import time
14 14
15 15 from ..i18n import _
16 16
17 17 from .common import (
18 18 ErrorResponse,
19 19 HTTP_SERVER_ERROR,
20 20 cspvalues,
21 21 get_contact,
22 22 get_mtime,
23 23 ismember,
24 24 paritygen,
25 25 staticfile,
26 26 statusmessage,
27 27 )
28 28
29 29 from .. import (
30 30 configitems,
31 31 encoding,
32 32 error,
33 33 extensions,
34 34 hg,
35 35 pathutil,
36 36 profiling,
37 37 pycompat,
38 38 registrar,
39 39 scmutil,
40 40 templater,
41 41 templateutil,
42 42 ui as uimod,
43 43 util,
44 44 )
45 45
46 46 from . import (
47 47 hgweb_mod,
48 48 request as requestmod,
49 49 webutil,
50 50 wsgicgi,
51 51 )
52 52 from ..utils import dateutil
53 53
54 54
55 55 def cleannames(items):
56 56 return [(util.pconvert(name).strip(b'/'), path) for name, path in items]
57 57
58 58
59 59 def findrepos(paths):
60 60 repos = []
61 61 for prefix, root in cleannames(paths):
62 62 roothead, roottail = os.path.split(root)
63 63 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
64 64 # /bar/ be served as as foo/N .
65 65 # '*' will not search inside dirs with .hg (except .hg/patches),
66 66 # '**' will search inside dirs with .hg (and thus also find subrepos).
67 67 try:
68 68 recurse = {b'*': False, b'**': True}[roottail]
69 69 except KeyError:
70 70 repos.append((prefix, root))
71 71 continue
72 72 roothead = os.path.normpath(os.path.abspath(roothead))
73 73 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
74 74 repos.extend(urlrepos(prefix, roothead, paths))
75 75 return repos
76 76
77 77
78 78 def urlrepos(prefix, roothead, paths):
79 79 """yield url paths and filesystem paths from a list of repo paths
80 80
81 81 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
82 82 >>> conv(urlrepos(b'hg', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
83 83 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
84 84 >>> conv(urlrepos(b'', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
85 85 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
86 86 """
87 87 for path in paths:
88 88 path = os.path.normpath(path)
89 89 yield (
90 90 prefix + b'/' + util.pconvert(path[len(roothead) :]).lstrip(b'/')
91 91 ).strip(b'/'), path
92 92
93 93
94 94 def readallowed(ui, req):
95 95 """Check allow_read and deny_read config options of a repo's ui object
96 96 to determine user permissions. By default, with neither option set (or
97 97 both empty), allow all users to read the repo. There are two ways a
98 98 user can be denied read access: (1) deny_read is not empty, and the
99 99 user is unauthenticated or deny_read contains user (or *), and (2)
100 100 allow_read is not empty and the user is not in allow_read. Return True
101 101 if user is allowed to read the repo, else return False."""
102 102
103 103 user = req.remoteuser
104 104
105 105 deny_read = ui.configlist(b'web', b'deny_read', untrusted=True)
106 106 if deny_read and (not user or ismember(ui, user, deny_read)):
107 107 return False
108 108
109 109 allow_read = ui.configlist(b'web', b'allow_read', untrusted=True)
110 110 # by default, allow reading if no allow_read option has been set
111 111 if not allow_read or ismember(ui, user, allow_read):
112 112 return True
113 113
114 114 return False
115 115
116 116
117 117 def rawindexentries(ui, repos, req, subdir=b''):
118 118 descend = ui.configbool(b'web', b'descend')
119 119 collapse = ui.configbool(b'web', b'collapse')
120 120 seenrepos = set()
121 121 seendirs = set()
122 122 for name, path in repos:
123 123
124 124 if not name.startswith(subdir):
125 125 continue
126 126 name = name[len(subdir) :]
127 127 directory = False
128 128
129 129 if b'/' in name:
130 130 if not descend:
131 131 continue
132 132
133 133 nameparts = name.split(b'/')
134 134 rootname = nameparts[0]
135 135
136 136 if not collapse:
137 137 pass
138 138 elif rootname in seendirs:
139 139 continue
140 140 elif rootname in seenrepos:
141 141 pass
142 142 else:
143 143 directory = True
144 144 name = rootname
145 145
146 146 # redefine the path to refer to the directory
147 147 discarded = b'/'.join(nameparts[1:])
148 148
149 149 # remove name parts plus accompanying slash
150 150 path = path[: -len(discarded) - 1]
151 151
152 152 try:
153 153 hg.repository(ui, path)
154 154 directory = False
155 155 except (IOError, error.RepoError):
156 156 pass
157 157
158 158 parts = [
159 159 req.apppath.strip(b'/'),
160 160 subdir.strip(b'/'),
161 161 name.strip(b'/'),
162 162 ]
163 163 url = b'/' + b'/'.join(p for p in parts if p) + b'/'
164 164
165 165 # show either a directory entry or a repository
166 166 if directory:
167 167 # get the directory's time information
168 168 try:
169 169 d = (get_mtime(path), dateutil.makedate()[1])
170 170 except OSError:
171 171 continue
172 172
173 173 # add '/' to the name to make it obvious that
174 174 # the entry is a directory, not a regular repository
175 175 row = {
176 176 b'contact': b"",
177 177 b'contact_sort': b"",
178 178 b'name': name + b'/',
179 179 b'name_sort': name,
180 180 b'url': url,
181 181 b'description': b"",
182 182 b'description_sort': b"",
183 183 b'lastchange': d,
184 184 b'lastchange_sort': d[1] - d[0],
185 185 b'archives': templateutil.mappinglist([]),
186 186 b'isdirectory': True,
187 187 b'labels': templateutil.hybridlist([], name=b'label'),
188 188 }
189 189
190 190 seendirs.add(name)
191 191 yield row
192 192 continue
193 193
194 194 u = ui.copy()
195 195 try:
196 196 u.readconfig(os.path.join(path, b'.hg', b'hgrc'))
197 197 except Exception as e:
198 198 u.warn(_(b'error reading %s/.hg/hgrc: %s\n') % (path, e))
199 199 continue
200 200
201 201 def get(section, name, default=uimod._unset):
202 202 return u.config(section, name, default, untrusted=True)
203 203
204 204 if u.configbool(b"web", b"hidden", untrusted=True):
205 205 continue
206 206
207 207 if not readallowed(u, req):
208 208 continue
209 209
210 210 # update time with local timezone
211 211 try:
212 212 r = hg.repository(ui, path)
213 213 except IOError:
214 214 u.warn(_(b'error accessing repository at %s\n') % path)
215 215 continue
216 216 except error.RepoError:
217 217 u.warn(_(b'error accessing repository at %s\n') % path)
218 218 continue
219 219 try:
220 220 d = (get_mtime(r.spath), dateutil.makedate()[1])
221 221 except OSError:
222 222 continue
223 223
224 224 contact = get_contact(get)
225 225 description = get(b"web", b"description")
226 226 seenrepos.add(name)
227 227 name = get(b"web", b"name", name)
228 228 labels = u.configlist(b'web', b'labels', untrusted=True)
229 229 row = {
230 230 b'contact': contact or b"unknown",
231 231 b'contact_sort': contact.upper() or b"unknown",
232 232 b'name': name,
233 233 b'name_sort': name,
234 234 b'url': url,
235 235 b'description': description or b"unknown",
236 236 b'description_sort': description.upper() or b"unknown",
237 237 b'lastchange': d,
238 238 b'lastchange_sort': d[1] - d[0],
239 239 b'archives': webutil.archivelist(u, b"tip", url),
240 240 b'isdirectory': None,
241 241 b'labels': templateutil.hybridlist(labels, name=b'label'),
242 242 }
243 243
244 244 yield row
245 245
246 246
247 247 def _indexentriesgen(
248 248 context, ui, repos, req, stripecount, sortcolumn, descending, subdir
249 249 ):
250 250 rows = rawindexentries(ui, repos, req, subdir=subdir)
251 251
252 252 sortdefault = None, False
253 253
254 254 if sortcolumn and sortdefault != (sortcolumn, descending):
255 255 sortkey = b'%s_sort' % sortcolumn
256 256 rows = sorted(rows, key=lambda x: x[sortkey], reverse=descending)
257 257
258 258 for row, parity in zip(rows, paritygen(stripecount)):
259 259 row[b'parity'] = parity
260 260 yield row
261 261
262 262
263 263 def indexentries(
264 264 ui, repos, req, stripecount, sortcolumn=b'', descending=False, subdir=b''
265 265 ):
266 266 args = (ui, repos, req, stripecount, sortcolumn, descending, subdir)
267 267 return templateutil.mappinggenerator(_indexentriesgen, args=args)
268 268
269 269
270 270 class hgwebdir(object):
271 271 """HTTP server for multiple repositories.
272 272
273 273 Given a configuration, different repositories will be served depending
274 274 on the request path.
275 275
276 276 Instances are typically used as WSGI applications.
277 277 """
278 278
279 279 def __init__(self, conf, baseui=None):
280 280 self.conf = conf
281 281 self.baseui = baseui
282 282 self.ui = None
283 283 self.lastrefresh = 0
284 284 self.motd = None
285 285 self.refresh()
286 286 if not baseui:
287 287 # set up environment for new ui
288 288 extensions.loadall(self.ui)
289 289 extensions.populateui(self.ui)
290 290
291 291 def refresh(self):
292 292 if self.ui:
293 293 refreshinterval = self.ui.configint(b'web', b'refreshinterval')
294 294 else:
295 295 item = configitems.coreitems[b'web'][b'refreshinterval']
296 296 refreshinterval = item.default
297 297
298 298 # refreshinterval <= 0 means to always refresh.
299 299 if (
300 300 refreshinterval > 0
301 301 and self.lastrefresh + refreshinterval > time.time()
302 302 ):
303 303 return
304 304
305 305 if self.baseui:
306 306 u = self.baseui.copy()
307 307 else:
308 308 u = uimod.ui.load()
309 309 u.setconfig(b'ui', b'report_untrusted', b'off', b'hgwebdir')
310 310 u.setconfig(b'ui', b'nontty', b'true', b'hgwebdir')
311 311 # displaying bundling progress bar while serving feels wrong and may
312 312 # break some wsgi implementations.
313 313 u.setconfig(b'progress', b'disable', b'true', b'hgweb')
314 314
315 315 if not isinstance(self.conf, (dict, list, tuple)):
316 316 map = {b'paths': b'hgweb-paths'}
317 317 if not os.path.exists(self.conf):
318 318 raise error.Abort(_(b'config file %s not found!') % self.conf)
319 319 u.readconfig(self.conf, remap=map, trust=True)
320 320 paths = []
321 321 for name, ignored in u.configitems(b'hgweb-paths'):
322 322 for path in u.configlist(b'hgweb-paths', name):
323 323 paths.append((name, path))
324 324 elif isinstance(self.conf, (list, tuple)):
325 325 paths = self.conf
326 326 elif isinstance(self.conf, dict):
327 327 paths = self.conf.items()
328 328 extensions.populateui(u)
329 329
330 330 repos = findrepos(paths)
331 331 for prefix, root in u.configitems(b'collections'):
332 332 prefix = util.pconvert(prefix)
333 333 for path in scmutil.walkrepos(root, followsym=True):
334 334 repo = os.path.normpath(path)
335 335 name = util.pconvert(repo)
336 336 if name.startswith(prefix):
337 337 name = name[len(prefix) :]
338 338 repos.append((name.lstrip(b'/'), repo))
339 339
340 340 self.repos = repos
341 341 self.ui = u
342 342 encoding.encoding = self.ui.config(b'web', b'encoding')
343 343 self.style = self.ui.config(b'web', b'style')
344 344 self.templatepath = self.ui.config(
345 345 b'web', b'templates', untrusted=False
346 346 )
347 347 self.stripecount = self.ui.config(b'web', b'stripes')
348 348 if self.stripecount:
349 349 self.stripecount = int(self.stripecount)
350 350 prefix = self.ui.config(b'web', b'prefix')
351 351 if prefix.startswith(b'/'):
352 352 prefix = prefix[1:]
353 353 if prefix.endswith(b'/'):
354 354 prefix = prefix[:-1]
355 355 self.prefix = prefix
356 356 self.lastrefresh = time.time()
357 357
358 358 def run(self):
359 359 if not encoding.environ.get(b'GATEWAY_INTERFACE', b'').startswith(
360 360 b"CGI/1."
361 361 ):
362 362 raise RuntimeError(
363 363 b"This function is only intended to be "
364 364 b"called while running as a CGI script."
365 365 )
366 366 wsgicgi.launch(self)
367 367
368 368 def __call__(self, env, respond):
369 369 baseurl = self.ui.config(b'web', b'baseurl')
370 370 req = requestmod.parserequestfromenv(env, altbaseurl=baseurl)
371 371 res = requestmod.wsgiresponse(req, respond)
372 372
373 373 return self.run_wsgi(req, res)
374 374
375 375 def run_wsgi(self, req, res):
376 376 profile = self.ui.configbool(b'profiling', b'enabled')
377 377 with profiling.profile(self.ui, enabled=profile):
378 378 try:
379 379 for r in self._runwsgi(req, res):
380 380 yield r
381 381 finally:
382 382 # There are known cycles in localrepository that prevent
383 383 # those objects (and tons of held references) from being
384 384 # collected through normal refcounting. We mitigate those
385 385 # leaks by performing an explicit GC on every request.
386 386 # TODO remove this once leaks are fixed.
387 387 # TODO only run this on requests that create localrepository
388 388 # instances instead of every request.
389 389 gc.collect()
390 390
391 391 def _runwsgi(self, req, res):
392 392 try:
393 393 self.refresh()
394 394
395 395 csp, nonce = cspvalues(self.ui)
396 396 if csp:
397 397 res.headers[b'Content-Security-Policy'] = csp
398 398
399 399 virtual = req.dispatchpath.strip(b'/')
400 400 tmpl = self.templater(req, nonce)
401 401 ctype = tmpl.render(b'mimetype', {b'encoding': encoding.encoding})
402 402
403 403 # Global defaults. These can be overridden by any handler.
404 404 res.status = b'200 Script output follows'
405 405 res.headers[b'Content-Type'] = ctype
406 406
407 407 # a static file
408 408 if virtual.startswith(b'static/') or b'static' in req.qsparams:
409 409 if virtual.startswith(b'static/'):
410 410 fname = virtual[7:]
411 411 else:
412 412 fname = req.qsparams[b'static']
413 413 static = self.ui.config(b"web", b"static", untrusted=False)
414 414 if not static:
415 415 tp = self.templatepath or templater.templatepaths()
416 if isinstance(tp, str):
416 if isinstance(tp, bytes):
417 417 tp = [tp]
418 418 static = [os.path.join(p, b'static') for p in tp]
419 419
420 420 staticfile(static, fname, res)
421 421 return res.sendresponse()
422 422
423 423 # top-level index
424 424
425 425 repos = dict(self.repos)
426 426
427 427 if (not virtual or virtual == b'index') and virtual not in repos:
428 428 return self.makeindex(req, res, tmpl)
429 429
430 430 # nested indexes and hgwebs
431 431
432 432 if virtual.endswith(b'/index') and virtual not in repos:
433 433 subdir = virtual[: -len(b'index')]
434 434 if any(r.startswith(subdir) for r in repos):
435 435 return self.makeindex(req, res, tmpl, subdir)
436 436
437 437 def _virtualdirs():
438 438 # Check the full virtual path, and each parent
439 439 yield virtual
440 440 for p in pathutil.finddirs(virtual):
441 441 yield p
442 442
443 443 for virtualrepo in _virtualdirs():
444 444 real = repos.get(virtualrepo)
445 445 if real:
446 446 # Re-parse the WSGI environment to take into account our
447 447 # repository path component.
448 448 uenv = req.rawenv
449 449 if pycompat.ispy3:
450 450 uenv = {
451 451 k.decode('latin1'): v
452 452 for k, v in pycompat.iteritems(uenv)
453 453 }
454 454 req = requestmod.parserequestfromenv(
455 455 uenv,
456 456 reponame=virtualrepo,
457 457 altbaseurl=self.ui.config(b'web', b'baseurl'),
458 458 # Reuse wrapped body file object otherwise state
459 459 # tracking can get confused.
460 460 bodyfh=req.bodyfh,
461 461 )
462 462 try:
463 463 # ensure caller gets private copy of ui
464 464 repo = hg.repository(self.ui.copy(), real)
465 465 return hgweb_mod.hgweb(repo).run_wsgi(req, res)
466 466 except IOError as inst:
467 467 msg = encoding.strtolocal(inst.strerror)
468 468 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
469 469 except error.RepoError as inst:
470 470 raise ErrorResponse(HTTP_SERVER_ERROR, bytes(inst))
471 471
472 472 # browse subdirectories
473 473 subdir = virtual + b'/'
474 474 if [r for r in repos if r.startswith(subdir)]:
475 475 return self.makeindex(req, res, tmpl, subdir)
476 476
477 477 # prefixes not found
478 478 res.status = b'404 Not Found'
479 479 res.setbodygen(tmpl.generate(b'notfound', {b'repo': virtual}))
480 480 return res.sendresponse()
481 481
482 482 except ErrorResponse as e:
483 483 res.status = statusmessage(e.code, pycompat.bytestr(e))
484 484 res.setbodygen(
485 485 tmpl.generate(b'error', {b'error': e.message or b''})
486 486 )
487 487 return res.sendresponse()
488 488 finally:
489 489 tmpl = None
490 490
491 491 def makeindex(self, req, res, tmpl, subdir=b""):
492 492 self.refresh()
493 493 sortable = [b"name", b"description", b"contact", b"lastchange"]
494 494 sortcolumn, descending = None, False
495 495 if b'sort' in req.qsparams:
496 496 sortcolumn = req.qsparams[b'sort']
497 497 descending = sortcolumn.startswith(b'-')
498 498 if descending:
499 499 sortcolumn = sortcolumn[1:]
500 500 if sortcolumn not in sortable:
501 501 sortcolumn = b""
502 502
503 503 sort = [
504 504 (
505 505 b"sort_%s" % column,
506 506 b"%s%s"
507 507 % (
508 508 (not descending and column == sortcolumn) and b"-" or b"",
509 509 column,
510 510 ),
511 511 )
512 512 for column in sortable
513 513 ]
514 514
515 515 self.refresh()
516 516
517 517 entries = indexentries(
518 518 self.ui,
519 519 self.repos,
520 520 req,
521 521 self.stripecount,
522 522 sortcolumn=sortcolumn,
523 523 descending=descending,
524 524 subdir=subdir,
525 525 )
526 526
527 527 mapping = {
528 528 b'entries': entries,
529 529 b'subdir': subdir,
530 530 b'pathdef': hgweb_mod.makebreadcrumb(b'/' + subdir, self.prefix),
531 531 b'sortcolumn': sortcolumn,
532 532 b'descending': descending,
533 533 }
534 534 mapping.update(sort)
535 535 res.setbodygen(tmpl.generate(b'index', mapping))
536 536 return res.sendresponse()
537 537
538 538 def templater(self, req, nonce):
539 539 def config(section, name, default=uimod._unset, untrusted=True):
540 540 return self.ui.config(section, name, default, untrusted)
541 541
542 542 vars = {}
543 543 styles, (style, mapfile) = hgweb_mod.getstyle(
544 544 req, config, self.templatepath
545 545 )
546 546 if style == styles[0]:
547 547 vars[b'style'] = style
548 548
549 549 sessionvars = webutil.sessionvars(vars, b'?')
550 550 logourl = config(b'web', b'logourl')
551 551 logoimg = config(b'web', b'logoimg')
552 552 staticurl = (
553 553 config(b'web', b'staticurl')
554 554 or req.apppath.rstrip(b'/') + b'/static/'
555 555 )
556 556 if not staticurl.endswith(b'/'):
557 557 staticurl += b'/'
558 558
559 559 defaults = {
560 560 b"encoding": encoding.encoding,
561 561 b"url": req.apppath + b'/',
562 562 b"logourl": logourl,
563 563 b"logoimg": logoimg,
564 564 b"staticurl": staticurl,
565 565 b"sessionvars": sessionvars,
566 566 b"style": style,
567 567 b"nonce": nonce,
568 568 }
569 569 templatekeyword = registrar.templatekeyword(defaults)
570 570
571 571 @templatekeyword(b'motd', requires=())
572 572 def motd(context, mapping):
573 573 if self.motd is not None:
574 574 yield self.motd
575 575 else:
576 576 yield config(b'web', b'motd')
577 577
578 578 tmpl = templater.templater.frommapfile(mapfile, defaults=defaults)
579 579 return tmpl
@@ -1,1599 +1,1599 b''
1 1 #
2 2 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import copy
11 11 import mimetypes
12 12 import os
13 13 import re
14 14
15 15 from ..i18n import _
16 16 from ..node import hex, short
17 17 from ..pycompat import getattr
18 18
19 19 from .common import (
20 20 ErrorResponse,
21 21 HTTP_FORBIDDEN,
22 22 HTTP_NOT_FOUND,
23 23 get_contact,
24 24 paritygen,
25 25 staticfile,
26 26 )
27 27
28 28 from .. import (
29 29 archival,
30 30 dagop,
31 31 encoding,
32 32 error,
33 33 graphmod,
34 34 pycompat,
35 35 revset,
36 36 revsetlang,
37 37 scmutil,
38 38 smartset,
39 39 templater,
40 40 templateutil,
41 41 )
42 42
43 43 from ..utils import stringutil
44 44
45 45 from . import webutil
46 46
47 47 __all__ = []
48 48 commands = {}
49 49
50 50
51 51 class webcommand(object):
52 52 """Decorator used to register a web command handler.
53 53
54 54 The decorator takes as its positional arguments the name/path the
55 55 command should be accessible under.
56 56
57 57 When called, functions receive as arguments a ``requestcontext``,
58 58 ``wsgirequest``, and a templater instance for generatoring output.
59 59 The functions should populate the ``rctx.res`` object with details
60 60 about the HTTP response.
61 61
62 62 The function returns a generator to be consumed by the WSGI application.
63 63 For most commands, this should be the result from
64 64 ``web.res.sendresponse()``. Many commands will call ``web.sendtemplate()``
65 65 to render a template.
66 66
67 67 Usage:
68 68
69 69 @webcommand('mycommand')
70 70 def mycommand(web):
71 71 pass
72 72 """
73 73
74 74 def __init__(self, name):
75 75 self.name = name
76 76
77 77 def __call__(self, func):
78 78 __all__.append(self.name)
79 79 commands[self.name] = func
80 80 return func
81 81
82 82
83 83 @webcommand(b'log')
84 84 def log(web):
85 85 """
86 86 /log[/{revision}[/{path}]]
87 87 --------------------------
88 88
89 89 Show repository or file history.
90 90
91 91 For URLs of the form ``/log/{revision}``, a list of changesets starting at
92 92 the specified changeset identifier is shown. If ``{revision}`` is not
93 93 defined, the default is ``tip``. This form is equivalent to the
94 94 ``changelog`` handler.
95 95
96 96 For URLs of the form ``/log/{revision}/{file}``, the history for a specific
97 97 file will be shown. This form is equivalent to the ``filelog`` handler.
98 98 """
99 99
100 100 if web.req.qsparams.get(b'file'):
101 101 return filelog(web)
102 102 else:
103 103 return changelog(web)
104 104
105 105
106 106 @webcommand(b'rawfile')
107 107 def rawfile(web):
108 108 guessmime = web.configbool(b'web', b'guessmime')
109 109
110 110 path = webutil.cleanpath(web.repo, web.req.qsparams.get(b'file', b''))
111 111 if not path:
112 112 return manifest(web)
113 113
114 114 try:
115 115 fctx = webutil.filectx(web.repo, web.req)
116 116 except error.LookupError as inst:
117 117 try:
118 118 return manifest(web)
119 119 except ErrorResponse:
120 120 raise inst
121 121
122 122 path = fctx.path()
123 123 text = fctx.data()
124 124 mt = b'application/binary'
125 125 if guessmime:
126 126 mt = mimetypes.guess_type(pycompat.fsdecode(path))[0]
127 127 if mt is None:
128 128 if stringutil.binary(text):
129 129 mt = b'application/binary'
130 130 else:
131 131 mt = b'text/plain'
132 132 else:
133 133 mt = pycompat.sysbytes(mt)
134 134
135 135 if mt.startswith(b'text/'):
136 136 mt += b'; charset="%s"' % encoding.encoding
137 137
138 138 web.res.headers[b'Content-Type'] = mt
139 139 filename = (
140 140 path.rpartition(b'/')[-1].replace(b'\\', b'\\\\').replace(b'"', b'\\"')
141 141 )
142 142 web.res.headers[b'Content-Disposition'] = (
143 143 b'inline; filename="%s"' % filename
144 144 )
145 145 web.res.setbodybytes(text)
146 146 return web.res.sendresponse()
147 147
148 148
149 149 def _filerevision(web, fctx):
150 150 f = fctx.path()
151 151 text = fctx.data()
152 152 parity = paritygen(web.stripecount)
153 153 ishead = fctx.filenode() in fctx.filelog().heads()
154 154
155 155 if stringutil.binary(text):
156 156 mt = pycompat.sysbytes(
157 157 mimetypes.guess_type(pycompat.fsdecode(f))[0]
158 158 or r'application/octet-stream'
159 159 )
160 160 text = b'(binary:%s)' % mt
161 161
162 162 def lines(context):
163 163 for lineno, t in enumerate(text.splitlines(True)):
164 164 yield {
165 165 b"line": t,
166 166 b"lineid": b"l%d" % (lineno + 1),
167 167 b"linenumber": b"% 6d" % (lineno + 1),
168 168 b"parity": next(parity),
169 169 }
170 170
171 171 return web.sendtemplate(
172 172 b'filerevision',
173 173 file=f,
174 174 path=webutil.up(f),
175 175 text=templateutil.mappinggenerator(lines),
176 176 symrev=webutil.symrevorshortnode(web.req, fctx),
177 177 rename=webutil.renamelink(fctx),
178 178 permissions=fctx.manifest().flags(f),
179 179 ishead=int(ishead),
180 180 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx))
181 181 )
182 182
183 183
184 184 @webcommand(b'file')
185 185 def file(web):
186 186 """
187 187 /file/{revision}[/{path}]
188 188 -------------------------
189 189
190 190 Show information about a directory or file in the repository.
191 191
192 192 Info about the ``path`` given as a URL parameter will be rendered.
193 193
194 194 If ``path`` is a directory, information about the entries in that
195 195 directory will be rendered. This form is equivalent to the ``manifest``
196 196 handler.
197 197
198 198 If ``path`` is a file, information about that file will be shown via
199 199 the ``filerevision`` template.
200 200
201 201 If ``path`` is not defined, information about the root directory will
202 202 be rendered.
203 203 """
204 204 if web.req.qsparams.get(b'style') == b'raw':
205 205 return rawfile(web)
206 206
207 207 path = webutil.cleanpath(web.repo, web.req.qsparams.get(b'file', b''))
208 208 if not path:
209 209 return manifest(web)
210 210 try:
211 211 return _filerevision(web, webutil.filectx(web.repo, web.req))
212 212 except error.LookupError as inst:
213 213 try:
214 214 return manifest(web)
215 215 except ErrorResponse:
216 216 raise inst
217 217
218 218
219 219 def _search(web):
220 220 MODE_REVISION = b'rev'
221 221 MODE_KEYWORD = b'keyword'
222 222 MODE_REVSET = b'revset'
223 223
224 224 def revsearch(ctx):
225 225 yield ctx
226 226
227 227 def keywordsearch(query):
228 228 lower = encoding.lower
229 229 qw = lower(query).split()
230 230
231 231 def revgen():
232 232 cl = web.repo.changelog
233 233 for i in pycompat.xrange(len(web.repo) - 1, 0, -100):
234 234 l = []
235 235 for j in cl.revs(max(0, i - 99), i):
236 236 ctx = web.repo[j]
237 237 l.append(ctx)
238 238 l.reverse()
239 239 for e in l:
240 240 yield e
241 241
242 242 for ctx in revgen():
243 243 miss = 0
244 244 for q in qw:
245 245 if not (
246 246 q in lower(ctx.user())
247 247 or q in lower(ctx.description())
248 248 or q in lower(b" ".join(ctx.files()))
249 249 ):
250 250 miss = 1
251 251 break
252 252 if miss:
253 253 continue
254 254
255 255 yield ctx
256 256
257 257 def revsetsearch(revs):
258 258 for r in revs:
259 259 yield web.repo[r]
260 260
261 261 searchfuncs = {
262 262 MODE_REVISION: (revsearch, b'exact revision search'),
263 263 MODE_KEYWORD: (keywordsearch, b'literal keyword search'),
264 264 MODE_REVSET: (revsetsearch, b'revset expression search'),
265 265 }
266 266
267 267 def getsearchmode(query):
268 268 try:
269 269 ctx = scmutil.revsymbol(web.repo, query)
270 270 except (error.RepoError, error.LookupError):
271 271 # query is not an exact revision pointer, need to
272 272 # decide if it's a revset expression or keywords
273 273 pass
274 274 else:
275 275 return MODE_REVISION, ctx
276 276
277 277 revdef = b'reverse(%s)' % query
278 278 try:
279 279 tree = revsetlang.parse(revdef)
280 280 except error.ParseError:
281 281 # can't parse to a revset tree
282 282 return MODE_KEYWORD, query
283 283
284 284 if revsetlang.depth(tree) <= 2:
285 285 # no revset syntax used
286 286 return MODE_KEYWORD, query
287 287
288 288 if any(
289 289 (token, (value or b'')[:3]) == (b'string', b're:')
290 290 for token, value, pos in revsetlang.tokenize(revdef)
291 291 ):
292 292 return MODE_KEYWORD, query
293 293
294 294 funcsused = revsetlang.funcsused(tree)
295 295 if not funcsused.issubset(revset.safesymbols):
296 296 return MODE_KEYWORD, query
297 297
298 298 try:
299 299 mfunc = revset.match(
300 300 web.repo.ui, revdef, lookup=revset.lookupfn(web.repo)
301 301 )
302 302 revs = mfunc(web.repo)
303 303 return MODE_REVSET, revs
304 304 # ParseError: wrongly placed tokens, wrongs arguments, etc
305 305 # RepoLookupError: no such revision, e.g. in 'revision:'
306 306 # Abort: bookmark/tag not exists
307 307 # LookupError: ambiguous identifier, e.g. in '(bc)' on a large repo
308 308 except (
309 309 error.ParseError,
310 310 error.RepoLookupError,
311 311 error.Abort,
312 312 LookupError,
313 313 ):
314 314 return MODE_KEYWORD, query
315 315
316 316 def changelist(context):
317 317 count = 0
318 318
319 319 for ctx in searchfunc[0](funcarg):
320 320 count += 1
321 321 n = scmutil.binnode(ctx)
322 322 showtags = webutil.showtag(web.repo, b'changelogtag', n)
323 323 files = webutil.listfilediffs(ctx.files(), n, web.maxfiles)
324 324
325 325 lm = webutil.commonentry(web.repo, ctx)
326 326 lm.update(
327 327 {
328 328 b'parity': next(parity),
329 329 b'changelogtag': showtags,
330 330 b'files': files,
331 331 }
332 332 )
333 333 yield lm
334 334
335 335 if count >= revcount:
336 336 break
337 337
338 338 query = web.req.qsparams[b'rev']
339 339 revcount = web.maxchanges
340 340 if b'revcount' in web.req.qsparams:
341 341 try:
342 342 revcount = int(web.req.qsparams.get(b'revcount', revcount))
343 343 revcount = max(revcount, 1)
344 344 web.tmpl.defaults[b'sessionvars'][b'revcount'] = revcount
345 345 except ValueError:
346 346 pass
347 347
348 348 lessvars = copy.copy(web.tmpl.defaults[b'sessionvars'])
349 349 lessvars[b'revcount'] = max(revcount // 2, 1)
350 350 lessvars[b'rev'] = query
351 351 morevars = copy.copy(web.tmpl.defaults[b'sessionvars'])
352 352 morevars[b'revcount'] = revcount * 2
353 353 morevars[b'rev'] = query
354 354
355 355 mode, funcarg = getsearchmode(query)
356 356
357 357 if b'forcekw' in web.req.qsparams:
358 358 showforcekw = b''
359 359 showunforcekw = searchfuncs[mode][1]
360 360 mode = MODE_KEYWORD
361 361 funcarg = query
362 362 else:
363 363 if mode != MODE_KEYWORD:
364 364 showforcekw = searchfuncs[MODE_KEYWORD][1]
365 365 else:
366 366 showforcekw = b''
367 367 showunforcekw = b''
368 368
369 369 searchfunc = searchfuncs[mode]
370 370
371 371 tip = web.repo[b'tip']
372 372 parity = paritygen(web.stripecount)
373 373
374 374 return web.sendtemplate(
375 375 b'search',
376 376 query=query,
377 377 node=tip.hex(),
378 378 symrev=b'tip',
379 379 entries=templateutil.mappinggenerator(changelist, name=b'searchentry'),
380 380 archives=web.archivelist(b'tip'),
381 381 morevars=morevars,
382 382 lessvars=lessvars,
383 383 modedesc=searchfunc[1],
384 384 showforcekw=showforcekw,
385 385 showunforcekw=showunforcekw,
386 386 )
387 387
388 388
389 389 @webcommand(b'changelog')
390 390 def changelog(web, shortlog=False):
391 391 """
392 392 /changelog[/{revision}]
393 393 -----------------------
394 394
395 395 Show information about multiple changesets.
396 396
397 397 If the optional ``revision`` URL argument is absent, information about
398 398 all changesets starting at ``tip`` will be rendered. If the ``revision``
399 399 argument is present, changesets will be shown starting from the specified
400 400 revision.
401 401
402 402 If ``revision`` is absent, the ``rev`` query string argument may be
403 403 defined. This will perform a search for changesets.
404 404
405 405 The argument for ``rev`` can be a single revision, a revision set,
406 406 or a literal keyword to search for in changeset data (equivalent to
407 407 :hg:`log -k`).
408 408
409 409 The ``revcount`` query string argument defines the maximum numbers of
410 410 changesets to render.
411 411
412 412 For non-searches, the ``changelog`` template will be rendered.
413 413 """
414 414
415 415 query = b''
416 416 if b'node' in web.req.qsparams:
417 417 ctx = webutil.changectx(web.repo, web.req)
418 418 symrev = webutil.symrevorshortnode(web.req, ctx)
419 419 elif b'rev' in web.req.qsparams:
420 420 return _search(web)
421 421 else:
422 422 ctx = web.repo[b'tip']
423 423 symrev = b'tip'
424 424
425 425 def changelist(maxcount):
426 426 revs = []
427 427 if pos != -1:
428 428 revs = web.repo.changelog.revs(pos, 0)
429 429
430 430 for entry in webutil.changelistentries(web, revs, maxcount, parity):
431 431 yield entry
432 432
433 433 if shortlog:
434 434 revcount = web.maxshortchanges
435 435 else:
436 436 revcount = web.maxchanges
437 437
438 438 if b'revcount' in web.req.qsparams:
439 439 try:
440 440 revcount = int(web.req.qsparams.get(b'revcount', revcount))
441 441 revcount = max(revcount, 1)
442 442 web.tmpl.defaults[b'sessionvars'][b'revcount'] = revcount
443 443 except ValueError:
444 444 pass
445 445
446 446 lessvars = copy.copy(web.tmpl.defaults[b'sessionvars'])
447 447 lessvars[b'revcount'] = max(revcount // 2, 1)
448 448 morevars = copy.copy(web.tmpl.defaults[b'sessionvars'])
449 449 morevars[b'revcount'] = revcount * 2
450 450
451 451 count = len(web.repo)
452 452 pos = ctx.rev()
453 453 parity = paritygen(web.stripecount)
454 454
455 455 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
456 456
457 457 entries = list(changelist(revcount + 1))
458 458 latestentry = entries[:1]
459 459 if len(entries) > revcount:
460 460 nextentry = entries[-1:]
461 461 entries = entries[:-1]
462 462 else:
463 463 nextentry = []
464 464
465 465 return web.sendtemplate(
466 466 b'shortlog' if shortlog else b'changelog',
467 467 changenav=changenav,
468 468 node=ctx.hex(),
469 469 rev=pos,
470 470 symrev=symrev,
471 471 changesets=count,
472 472 entries=templateutil.mappinglist(entries),
473 473 latestentry=templateutil.mappinglist(latestentry),
474 474 nextentry=templateutil.mappinglist(nextentry),
475 475 archives=web.archivelist(b'tip'),
476 476 revcount=revcount,
477 477 morevars=morevars,
478 478 lessvars=lessvars,
479 479 query=query,
480 480 )
481 481
482 482
483 483 @webcommand(b'shortlog')
484 484 def shortlog(web):
485 485 """
486 486 /shortlog
487 487 ---------
488 488
489 489 Show basic information about a set of changesets.
490 490
491 491 This accepts the same parameters as the ``changelog`` handler. The only
492 492 difference is the ``shortlog`` template will be rendered instead of the
493 493 ``changelog`` template.
494 494 """
495 495 return changelog(web, shortlog=True)
496 496
497 497
498 498 @webcommand(b'changeset')
499 499 def changeset(web):
500 500 """
501 501 /changeset[/{revision}]
502 502 -----------------------
503 503
504 504 Show information about a single changeset.
505 505
506 506 A URL path argument is the changeset identifier to show. See ``hg help
507 507 revisions`` for possible values. If not defined, the ``tip`` changeset
508 508 will be shown.
509 509
510 510 The ``changeset`` template is rendered. Contents of the ``changesettag``,
511 511 ``changesetbookmark``, ``filenodelink``, ``filenolink``, and the many
512 512 templates related to diffs may all be used to produce the output.
513 513 """
514 514 ctx = webutil.changectx(web.repo, web.req)
515 515
516 516 return web.sendtemplate(b'changeset', **webutil.changesetentry(web, ctx))
517 517
518 518
519 519 rev = webcommand(b'rev')(changeset)
520 520
521 521
522 522 def decodepath(path):
523 523 """Hook for mapping a path in the repository to a path in the
524 524 working copy.
525 525
526 526 Extensions (e.g., largefiles) can override this to remap files in
527 527 the virtual file system presented by the manifest command below."""
528 528 return path
529 529
530 530
531 531 @webcommand(b'manifest')
532 532 def manifest(web):
533 533 """
534 534 /manifest[/{revision}[/{path}]]
535 535 -------------------------------
536 536
537 537 Show information about a directory.
538 538
539 539 If the URL path arguments are omitted, information about the root
540 540 directory for the ``tip`` changeset will be shown.
541 541
542 542 Because this handler can only show information for directories, it
543 543 is recommended to use the ``file`` handler instead, as it can handle both
544 544 directories and files.
545 545
546 546 The ``manifest`` template will be rendered for this handler.
547 547 """
548 548 if b'node' in web.req.qsparams:
549 549 ctx = webutil.changectx(web.repo, web.req)
550 550 symrev = webutil.symrevorshortnode(web.req, ctx)
551 551 else:
552 552 ctx = web.repo[b'tip']
553 553 symrev = b'tip'
554 554 path = webutil.cleanpath(web.repo, web.req.qsparams.get(b'file', b''))
555 555 mf = ctx.manifest()
556 556 node = scmutil.binnode(ctx)
557 557
558 558 files = {}
559 559 dirs = {}
560 560 parity = paritygen(web.stripecount)
561 561
562 562 if path and path[-1:] != b"/":
563 563 path += b"/"
564 564 l = len(path)
565 565 abspath = b"/" + path
566 566
567 567 for full, n in pycompat.iteritems(mf):
568 568 # the virtual path (working copy path) used for the full
569 569 # (repository) path
570 570 f = decodepath(full)
571 571
572 572 if f[:l] != path:
573 573 continue
574 574 remain = f[l:]
575 575 elements = remain.split(b'/')
576 576 if len(elements) == 1:
577 577 files[remain] = full
578 578 else:
579 579 h = dirs # need to retain ref to dirs (root)
580 580 for elem in elements[0:-1]:
581 581 if elem not in h:
582 582 h[elem] = {}
583 583 h = h[elem]
584 584 if len(h) > 1:
585 585 break
586 586 h[None] = None # denotes files present
587 587
588 588 if mf and not files and not dirs:
589 589 raise ErrorResponse(HTTP_NOT_FOUND, b'path not found: ' + path)
590 590
591 591 def filelist(context):
592 592 for f in sorted(files):
593 593 full = files[f]
594 594
595 595 fctx = ctx.filectx(full)
596 596 yield {
597 597 b"file": full,
598 598 b"parity": next(parity),
599 599 b"basename": f,
600 600 b"date": fctx.date(),
601 601 b"size": fctx.size(),
602 602 b"permissions": mf.flags(full),
603 603 }
604 604
605 605 def dirlist(context):
606 606 for d in sorted(dirs):
607 607
608 608 emptydirs = []
609 609 h = dirs[d]
610 610 while isinstance(h, dict) and len(h) == 1:
611 611 k, v = next(iter(h.items()))
612 612 if v:
613 613 emptydirs.append(k)
614 614 h = v
615 615
616 616 path = b"%s%s" % (abspath, d)
617 617 yield {
618 618 b"parity": next(parity),
619 619 b"path": path,
620 620 b"emptydirs": b"/".join(emptydirs),
621 621 b"basename": d,
622 622 }
623 623
624 624 return web.sendtemplate(
625 625 b'manifest',
626 626 symrev=symrev,
627 627 path=abspath,
628 628 up=webutil.up(abspath),
629 629 upparity=next(parity),
630 630 fentries=templateutil.mappinggenerator(filelist),
631 631 dentries=templateutil.mappinggenerator(dirlist),
632 632 archives=web.archivelist(hex(node)),
633 633 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx))
634 634 )
635 635
636 636
637 637 @webcommand(b'tags')
638 638 def tags(web):
639 639 """
640 640 /tags
641 641 -----
642 642
643 643 Show information about tags.
644 644
645 645 No arguments are accepted.
646 646
647 647 The ``tags`` template is rendered.
648 648 """
649 649 i = list(reversed(web.repo.tagslist()))
650 650 parity = paritygen(web.stripecount)
651 651
652 652 def entries(context, notip, latestonly):
653 653 t = i
654 654 if notip:
655 655 t = [(k, n) for k, n in i if k != b"tip"]
656 656 if latestonly:
657 657 t = t[:1]
658 658 for k, n in t:
659 659 yield {
660 660 b"parity": next(parity),
661 661 b"tag": k,
662 662 b"date": web.repo[n].date(),
663 663 b"node": hex(n),
664 664 }
665 665
666 666 return web.sendtemplate(
667 667 b'tags',
668 668 node=hex(web.repo.changelog.tip()),
669 669 entries=templateutil.mappinggenerator(entries, args=(False, False)),
670 670 entriesnotip=templateutil.mappinggenerator(entries, args=(True, False)),
671 671 latestentry=templateutil.mappinggenerator(entries, args=(True, True)),
672 672 )
673 673
674 674
675 675 @webcommand(b'bookmarks')
676 676 def bookmarks(web):
677 677 """
678 678 /bookmarks
679 679 ----------
680 680
681 681 Show information about bookmarks.
682 682
683 683 No arguments are accepted.
684 684
685 685 The ``bookmarks`` template is rendered.
686 686 """
687 687 i = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
688 688 sortkey = lambda b: (web.repo[b[1]].rev(), b[0])
689 689 i = sorted(i, key=sortkey, reverse=True)
690 690 parity = paritygen(web.stripecount)
691 691
692 692 def entries(context, latestonly):
693 693 t = i
694 694 if latestonly:
695 695 t = i[:1]
696 696 for k, n in t:
697 697 yield {
698 698 b"parity": next(parity),
699 699 b"bookmark": k,
700 700 b"date": web.repo[n].date(),
701 701 b"node": hex(n),
702 702 }
703 703
704 704 if i:
705 705 latestrev = i[0][1]
706 706 else:
707 707 latestrev = -1
708 708 lastdate = web.repo[latestrev].date()
709 709
710 710 return web.sendtemplate(
711 711 b'bookmarks',
712 712 node=hex(web.repo.changelog.tip()),
713 713 lastchange=templateutil.mappinglist([{b'date': lastdate}]),
714 714 entries=templateutil.mappinggenerator(entries, args=(False,)),
715 715 latestentry=templateutil.mappinggenerator(entries, args=(True,)),
716 716 )
717 717
718 718
719 719 @webcommand(b'branches')
720 720 def branches(web):
721 721 """
722 722 /branches
723 723 ---------
724 724
725 725 Show information about branches.
726 726
727 727 All known branches are contained in the output, even closed branches.
728 728
729 729 No arguments are accepted.
730 730
731 731 The ``branches`` template is rendered.
732 732 """
733 733 entries = webutil.branchentries(web.repo, web.stripecount)
734 734 latestentry = webutil.branchentries(web.repo, web.stripecount, 1)
735 735
736 736 return web.sendtemplate(
737 737 b'branches',
738 738 node=hex(web.repo.changelog.tip()),
739 739 entries=entries,
740 740 latestentry=latestentry,
741 741 )
742 742
743 743
744 744 @webcommand(b'summary')
745 745 def summary(web):
746 746 """
747 747 /summary
748 748 --------
749 749
750 750 Show a summary of repository state.
751 751
752 752 Information about the latest changesets, bookmarks, tags, and branches
753 753 is captured by this handler.
754 754
755 755 The ``summary`` template is rendered.
756 756 """
757 757 i = reversed(web.repo.tagslist())
758 758
759 759 def tagentries(context):
760 760 parity = paritygen(web.stripecount)
761 761 count = 0
762 762 for k, n in i:
763 763 if k == b"tip": # skip tip
764 764 continue
765 765
766 766 count += 1
767 767 if count > 10: # limit to 10 tags
768 768 break
769 769
770 770 yield {
771 771 b'parity': next(parity),
772 772 b'tag': k,
773 773 b'node': hex(n),
774 774 b'date': web.repo[n].date(),
775 775 }
776 776
777 777 def bookmarks(context):
778 778 parity = paritygen(web.stripecount)
779 779 marks = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
780 780 sortkey = lambda b: (web.repo[b[1]].rev(), b[0])
781 781 marks = sorted(marks, key=sortkey, reverse=True)
782 782 for k, n in marks[:10]: # limit to 10 bookmarks
783 783 yield {
784 784 b'parity': next(parity),
785 785 b'bookmark': k,
786 786 b'date': web.repo[n].date(),
787 787 b'node': hex(n),
788 788 }
789 789
790 790 def changelist(context):
791 791 parity = paritygen(web.stripecount, offset=start - end)
792 792 l = [] # build a list in forward order for efficiency
793 793 revs = []
794 794 if start < end:
795 795 revs = web.repo.changelog.revs(start, end - 1)
796 796 for i in revs:
797 797 ctx = web.repo[i]
798 798 lm = webutil.commonentry(web.repo, ctx)
799 799 lm[b'parity'] = next(parity)
800 800 l.append(lm)
801 801
802 802 for entry in reversed(l):
803 803 yield entry
804 804
805 805 tip = web.repo[b'tip']
806 806 count = len(web.repo)
807 807 start = max(0, count - web.maxchanges)
808 808 end = min(count, start + web.maxchanges)
809 809
810 810 desc = web.config(b"web", b"description")
811 811 if not desc:
812 812 desc = b'unknown'
813 813 labels = web.configlist(b'web', b'labels')
814 814
815 815 return web.sendtemplate(
816 816 b'summary',
817 817 desc=desc,
818 818 owner=get_contact(web.config) or b'unknown',
819 819 lastchange=tip.date(),
820 820 tags=templateutil.mappinggenerator(tagentries, name=b'tagentry'),
821 821 bookmarks=templateutil.mappinggenerator(bookmarks),
822 822 branches=webutil.branchentries(web.repo, web.stripecount, 10),
823 823 shortlog=templateutil.mappinggenerator(
824 824 changelist, name=b'shortlogentry'
825 825 ),
826 826 node=tip.hex(),
827 827 symrev=b'tip',
828 828 archives=web.archivelist(b'tip'),
829 829 labels=templateutil.hybridlist(labels, name=b'label'),
830 830 )
831 831
832 832
833 833 @webcommand(b'filediff')
834 834 def filediff(web):
835 835 """
836 836 /diff/{revision}/{path}
837 837 -----------------------
838 838
839 839 Show how a file changed in a particular commit.
840 840
841 841 The ``filediff`` template is rendered.
842 842
843 843 This handler is registered under both the ``/diff`` and ``/filediff``
844 844 paths. ``/diff`` is used in modern code.
845 845 """
846 846 fctx, ctx = None, None
847 847 try:
848 848 fctx = webutil.filectx(web.repo, web.req)
849 849 except LookupError:
850 850 ctx = webutil.changectx(web.repo, web.req)
851 851 path = webutil.cleanpath(web.repo, web.req.qsparams[b'file'])
852 852 if path not in ctx.files():
853 853 raise
854 854
855 855 if fctx is not None:
856 856 path = fctx.path()
857 857 ctx = fctx.changectx()
858 858 basectx = ctx.p1()
859 859
860 860 style = web.config(b'web', b'style')
861 861 if b'style' in web.req.qsparams:
862 862 style = web.req.qsparams[b'style']
863 863
864 864 diffs = webutil.diffs(web, ctx, basectx, [path], style)
865 865 if fctx is not None:
866 866 rename = webutil.renamelink(fctx)
867 867 ctx = fctx
868 868 else:
869 869 rename = templateutil.mappinglist([])
870 870 ctx = ctx
871 871
872 872 return web.sendtemplate(
873 873 b'filediff',
874 874 file=path,
875 875 symrev=webutil.symrevorshortnode(web.req, ctx),
876 876 rename=rename,
877 877 diff=diffs,
878 878 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx))
879 879 )
880 880
881 881
882 882 diff = webcommand(b'diff')(filediff)
883 883
884 884
885 885 @webcommand(b'comparison')
886 886 def comparison(web):
887 887 """
888 888 /comparison/{revision}/{path}
889 889 -----------------------------
890 890
891 891 Show a comparison between the old and new versions of a file from changes
892 892 made on a particular revision.
893 893
894 894 This is similar to the ``diff`` handler. However, this form features
895 895 a split or side-by-side diff rather than a unified diff.
896 896
897 897 The ``context`` query string argument can be used to control the lines of
898 898 context in the diff.
899 899
900 900 The ``filecomparison`` template is rendered.
901 901 """
902 902 ctx = webutil.changectx(web.repo, web.req)
903 903 if b'file' not in web.req.qsparams:
904 904 raise ErrorResponse(HTTP_NOT_FOUND, b'file not given')
905 905 path = webutil.cleanpath(web.repo, web.req.qsparams[b'file'])
906 906
907 907 parsecontext = lambda v: v == b'full' and -1 or int(v)
908 908 if b'context' in web.req.qsparams:
909 909 context = parsecontext(web.req.qsparams[b'context'])
910 910 else:
911 911 context = parsecontext(web.config(b'web', b'comparisoncontext'))
912 912
913 913 def filelines(f):
914 914 if f.isbinary():
915 915 mt = pycompat.sysbytes(
916 916 mimetypes.guess_type(pycompat.fsdecode(f.path()))[0]
917 917 or r'application/octet-stream'
918 918 )
919 919 return [_(b'(binary file %s, hash: %s)') % (mt, hex(f.filenode()))]
920 920 return f.data().splitlines()
921 921
922 922 fctx = None
923 923 parent = ctx.p1()
924 924 leftrev = parent.rev()
925 925 leftnode = parent.node()
926 926 rightrev = ctx.rev()
927 927 rightnode = scmutil.binnode(ctx)
928 928 if path in ctx:
929 929 fctx = ctx[path]
930 930 rightlines = filelines(fctx)
931 931 if path not in parent:
932 932 leftlines = ()
933 933 else:
934 934 pfctx = parent[path]
935 935 leftlines = filelines(pfctx)
936 936 else:
937 937 rightlines = ()
938 938 pfctx = ctx.p1()[path]
939 939 leftlines = filelines(pfctx)
940 940
941 941 comparison = webutil.compare(context, leftlines, rightlines)
942 942 if fctx is not None:
943 943 rename = webutil.renamelink(fctx)
944 944 ctx = fctx
945 945 else:
946 946 rename = templateutil.mappinglist([])
947 947 ctx = ctx
948 948
949 949 return web.sendtemplate(
950 950 b'filecomparison',
951 951 file=path,
952 952 symrev=webutil.symrevorshortnode(web.req, ctx),
953 953 rename=rename,
954 954 leftrev=leftrev,
955 955 leftnode=hex(leftnode),
956 956 rightrev=rightrev,
957 957 rightnode=hex(rightnode),
958 958 comparison=comparison,
959 959 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx))
960 960 )
961 961
962 962
963 963 @webcommand(b'annotate')
964 964 def annotate(web):
965 965 """
966 966 /annotate/{revision}/{path}
967 967 ---------------------------
968 968
969 969 Show changeset information for each line in a file.
970 970
971 971 The ``ignorews``, ``ignorewsamount``, ``ignorewseol``, and
972 972 ``ignoreblanklines`` query string arguments have the same meaning as
973 973 their ``[annotate]`` config equivalents. It uses the hgrc boolean
974 974 parsing logic to interpret the value. e.g. ``0`` and ``false`` are
975 975 false and ``1`` and ``true`` are true. If not defined, the server
976 976 default settings are used.
977 977
978 978 The ``fileannotate`` template is rendered.
979 979 """
980 980 fctx = webutil.filectx(web.repo, web.req)
981 981 f = fctx.path()
982 982 parity = paritygen(web.stripecount)
983 983 ishead = fctx.filenode() in fctx.filelog().heads()
984 984
985 985 # parents() is called once per line and several lines likely belong to
986 986 # same revision. So it is worth caching.
987 987 # TODO there are still redundant operations within basefilectx.parents()
988 988 # and from the fctx.annotate() call itself that could be cached.
989 989 parentscache = {}
990 990
991 991 def parents(context, f):
992 992 rev = f.rev()
993 993 if rev not in parentscache:
994 994 parentscache[rev] = []
995 995 for p in f.parents():
996 996 entry = {
997 997 b'node': p.hex(),
998 998 b'rev': p.rev(),
999 999 }
1000 1000 parentscache[rev].append(entry)
1001 1001
1002 1002 for p in parentscache[rev]:
1003 1003 yield p
1004 1004
1005 1005 def annotate(context):
1006 1006 if fctx.isbinary():
1007 1007 mt = pycompat.sysbytes(
1008 1008 mimetypes.guess_type(pycompat.fsdecode(fctx.path()))[0]
1009 1009 or r'application/octet-stream'
1010 1010 )
1011 1011 lines = [
1012 1012 dagop.annotateline(
1013 1013 fctx=fctx.filectx(fctx.filerev()),
1014 1014 lineno=1,
1015 1015 text=b'(binary:%s)' % mt,
1016 1016 )
1017 1017 ]
1018 1018 else:
1019 1019 lines = webutil.annotate(web.req, fctx, web.repo.ui)
1020 1020
1021 1021 previousrev = None
1022 1022 blockparitygen = paritygen(1)
1023 1023 for lineno, aline in enumerate(lines):
1024 1024 f = aline.fctx
1025 1025 rev = f.rev()
1026 1026 if rev != previousrev:
1027 1027 blockhead = True
1028 1028 blockparity = next(blockparitygen)
1029 1029 else:
1030 1030 blockhead = None
1031 1031 previousrev = rev
1032 1032 yield {
1033 1033 b"parity": next(parity),
1034 1034 b"node": f.hex(),
1035 1035 b"rev": rev,
1036 1036 b"author": f.user(),
1037 1037 b"parents": templateutil.mappinggenerator(parents, args=(f,)),
1038 1038 b"desc": f.description(),
1039 1039 b"extra": f.extra(),
1040 1040 b"file": f.path(),
1041 1041 b"blockhead": blockhead,
1042 1042 b"blockparity": blockparity,
1043 1043 b"targetline": aline.lineno,
1044 1044 b"line": aline.text,
1045 1045 b"lineno": lineno + 1,
1046 1046 b"lineid": b"l%d" % (lineno + 1),
1047 1047 b"linenumber": b"% 6d" % (lineno + 1),
1048 1048 b"revdate": f.date(),
1049 1049 }
1050 1050
1051 1051 diffopts = webutil.difffeatureopts(web.req, web.repo.ui, b'annotate')
1052 1052 diffopts = {k: getattr(diffopts, k) for k in diffopts.defaults}
1053 1053
1054 1054 return web.sendtemplate(
1055 1055 b'fileannotate',
1056 1056 file=f,
1057 1057 annotate=templateutil.mappinggenerator(annotate),
1058 1058 path=webutil.up(f),
1059 1059 symrev=webutil.symrevorshortnode(web.req, fctx),
1060 1060 rename=webutil.renamelink(fctx),
1061 1061 permissions=fctx.manifest().flags(f),
1062 1062 ishead=int(ishead),
1063 1063 diffopts=templateutil.hybriddict(diffopts),
1064 1064 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx))
1065 1065 )
1066 1066
1067 1067
1068 1068 @webcommand(b'filelog')
1069 1069 def filelog(web):
1070 1070 """
1071 1071 /filelog/{revision}/{path}
1072 1072 --------------------------
1073 1073
1074 1074 Show information about the history of a file in the repository.
1075 1075
1076 1076 The ``revcount`` query string argument can be defined to control the
1077 1077 maximum number of entries to show.
1078 1078
1079 1079 The ``filelog`` template will be rendered.
1080 1080 """
1081 1081
1082 1082 try:
1083 1083 fctx = webutil.filectx(web.repo, web.req)
1084 1084 f = fctx.path()
1085 1085 fl = fctx.filelog()
1086 1086 except error.LookupError:
1087 1087 f = webutil.cleanpath(web.repo, web.req.qsparams[b'file'])
1088 1088 fl = web.repo.file(f)
1089 1089 numrevs = len(fl)
1090 1090 if not numrevs: # file doesn't exist at all
1091 1091 raise
1092 1092 rev = webutil.changectx(web.repo, web.req).rev()
1093 1093 first = fl.linkrev(0)
1094 1094 if rev < first: # current rev is from before file existed
1095 1095 raise
1096 1096 frev = numrevs - 1
1097 1097 while fl.linkrev(frev) > rev:
1098 1098 frev -= 1
1099 1099 fctx = web.repo.filectx(f, fl.linkrev(frev))
1100 1100
1101 1101 revcount = web.maxshortchanges
1102 1102 if b'revcount' in web.req.qsparams:
1103 1103 try:
1104 1104 revcount = int(web.req.qsparams.get(b'revcount', revcount))
1105 1105 revcount = max(revcount, 1)
1106 1106 web.tmpl.defaults[b'sessionvars'][b'revcount'] = revcount
1107 1107 except ValueError:
1108 1108 pass
1109 1109
1110 1110 lrange = webutil.linerange(web.req)
1111 1111
1112 1112 lessvars = copy.copy(web.tmpl.defaults[b'sessionvars'])
1113 1113 lessvars[b'revcount'] = max(revcount // 2, 1)
1114 1114 morevars = copy.copy(web.tmpl.defaults[b'sessionvars'])
1115 1115 morevars[b'revcount'] = revcount * 2
1116 1116
1117 1117 patch = b'patch' in web.req.qsparams
1118 1118 if patch:
1119 1119 lessvars[b'patch'] = morevars[b'patch'] = web.req.qsparams[b'patch']
1120 1120 descend = b'descend' in web.req.qsparams
1121 1121 if descend:
1122 1122 lessvars[b'descend'] = morevars[b'descend'] = web.req.qsparams[
1123 1123 b'descend'
1124 1124 ]
1125 1125
1126 1126 count = fctx.filerev() + 1
1127 1127 start = max(0, count - revcount) # first rev on this page
1128 1128 end = min(count, start + revcount) # last rev on this page
1129 1129 parity = paritygen(web.stripecount, offset=start - end)
1130 1130
1131 1131 repo = web.repo
1132 1132 filelog = fctx.filelog()
1133 1133 revs = [
1134 1134 filerev
1135 1135 for filerev in filelog.revs(start, end - 1)
1136 1136 if filelog.linkrev(filerev) in repo
1137 1137 ]
1138 1138 entries = []
1139 1139
1140 1140 diffstyle = web.config(b'web', b'style')
1141 1141 if b'style' in web.req.qsparams:
1142 1142 diffstyle = web.req.qsparams[b'style']
1143 1143
1144 1144 def diff(fctx, linerange=None):
1145 1145 ctx = fctx.changectx()
1146 1146 basectx = ctx.p1()
1147 1147 path = fctx.path()
1148 1148 return webutil.diffs(
1149 1149 web,
1150 1150 ctx,
1151 1151 basectx,
1152 1152 [path],
1153 1153 diffstyle,
1154 1154 linerange=linerange,
1155 1155 lineidprefix=b'%s-' % ctx.hex()[:12],
1156 1156 )
1157 1157
1158 1158 linerange = None
1159 1159 if lrange is not None:
1160 1160 linerange = webutil.formatlinerange(*lrange)
1161 1161 # deactivate numeric nav links when linerange is specified as this
1162 1162 # would required a dedicated "revnav" class
1163 1163 nav = templateutil.mappinglist([])
1164 1164 if descend:
1165 1165 it = dagop.blockdescendants(fctx, *lrange)
1166 1166 else:
1167 1167 it = dagop.blockancestors(fctx, *lrange)
1168 1168 for i, (c, lr) in enumerate(it, 1):
1169 1169 diffs = None
1170 1170 if patch:
1171 1171 diffs = diff(c, linerange=lr)
1172 1172 # follow renames accross filtered (not in range) revisions
1173 1173 path = c.path()
1174 1174 lm = webutil.commonentry(repo, c)
1175 1175 lm.update(
1176 1176 {
1177 1177 b'parity': next(parity),
1178 1178 b'filerev': c.rev(),
1179 1179 b'file': path,
1180 1180 b'diff': diffs,
1181 1181 b'linerange': webutil.formatlinerange(*lr),
1182 1182 b'rename': templateutil.mappinglist([]),
1183 1183 }
1184 1184 )
1185 1185 entries.append(lm)
1186 1186 if i == revcount:
1187 1187 break
1188 1188 lessvars[b'linerange'] = webutil.formatlinerange(*lrange)
1189 1189 morevars[b'linerange'] = lessvars[b'linerange']
1190 1190 else:
1191 1191 for i in revs:
1192 1192 iterfctx = fctx.filectx(i)
1193 1193 diffs = None
1194 1194 if patch:
1195 1195 diffs = diff(iterfctx)
1196 1196 lm = webutil.commonentry(repo, iterfctx)
1197 1197 lm.update(
1198 1198 {
1199 1199 b'parity': next(parity),
1200 1200 b'filerev': i,
1201 1201 b'file': f,
1202 1202 b'diff': diffs,
1203 1203 b'rename': webutil.renamelink(iterfctx),
1204 1204 }
1205 1205 )
1206 1206 entries.append(lm)
1207 1207 entries.reverse()
1208 1208 revnav = webutil.filerevnav(web.repo, fctx.path())
1209 1209 nav = revnav.gen(end - 1, revcount, count)
1210 1210
1211 1211 latestentry = entries[:1]
1212 1212
1213 1213 return web.sendtemplate(
1214 1214 b'filelog',
1215 1215 file=f,
1216 1216 nav=nav,
1217 1217 symrev=webutil.symrevorshortnode(web.req, fctx),
1218 1218 entries=templateutil.mappinglist(entries),
1219 1219 descend=descend,
1220 1220 patch=patch,
1221 1221 latestentry=templateutil.mappinglist(latestentry),
1222 1222 linerange=linerange,
1223 1223 revcount=revcount,
1224 1224 morevars=morevars,
1225 1225 lessvars=lessvars,
1226 1226 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx))
1227 1227 )
1228 1228
1229 1229
1230 1230 @webcommand(b'archive')
1231 1231 def archive(web):
1232 1232 """
1233 1233 /archive/{revision}.{format}[/{path}]
1234 1234 -------------------------------------
1235 1235
1236 1236 Obtain an archive of repository content.
1237 1237
1238 1238 The content and type of the archive is defined by a URL path parameter.
1239 1239 ``format`` is the file extension of the archive type to be generated. e.g.
1240 1240 ``zip`` or ``tar.bz2``. Not all archive types may be allowed by your
1241 1241 server configuration.
1242 1242
1243 1243 The optional ``path`` URL parameter controls content to include in the
1244 1244 archive. If omitted, every file in the specified revision is present in the
1245 1245 archive. If included, only the specified file or contents of the specified
1246 1246 directory will be included in the archive.
1247 1247
1248 1248 No template is used for this handler. Raw, binary content is generated.
1249 1249 """
1250 1250
1251 1251 type_ = web.req.qsparams.get(b'type')
1252 1252 allowed = web.configlist(b"web", b"allow-archive")
1253 1253 key = web.req.qsparams[b'node']
1254 1254
1255 1255 if type_ not in webutil.archivespecs:
1256 1256 msg = b'Unsupported archive type: %s' % stringutil.pprint(type_)
1257 1257 raise ErrorResponse(HTTP_NOT_FOUND, msg)
1258 1258
1259 1259 if not ((type_ in allowed or web.configbool(b"web", b"allow" + type_))):
1260 1260 msg = b'Archive type not allowed: %s' % type_
1261 1261 raise ErrorResponse(HTTP_FORBIDDEN, msg)
1262 1262
1263 1263 reponame = re.sub(br"\W+", b"-", os.path.basename(web.reponame))
1264 1264 cnode = web.repo.lookup(key)
1265 1265 arch_version = key
1266 1266 if cnode == key or key == b'tip':
1267 1267 arch_version = short(cnode)
1268 1268 name = b"%s-%s" % (reponame, arch_version)
1269 1269
1270 1270 ctx = webutil.changectx(web.repo, web.req)
1271 1271 pats = []
1272 1272 match = scmutil.match(ctx, [])
1273 1273 file = web.req.qsparams.get(b'file')
1274 1274 if file:
1275 1275 pats = [b'path:' + file]
1276 1276 match = scmutil.match(ctx, pats, default=b'path')
1277 1277 if pats:
1278 1278 files = [f for f in ctx.manifest().keys() if match(f)]
1279 1279 if not files:
1280 1280 raise ErrorResponse(
1281 1281 HTTP_NOT_FOUND, b'file(s) not found: %s' % file
1282 1282 )
1283 1283
1284 1284 mimetype, artype, extension, encoding = webutil.archivespecs[type_]
1285 1285
1286 1286 web.res.headers[b'Content-Type'] = mimetype
1287 1287 web.res.headers[b'Content-Disposition'] = b'attachment; filename=%s%s' % (
1288 1288 name,
1289 1289 extension,
1290 1290 )
1291 1291
1292 1292 if encoding:
1293 1293 web.res.headers[b'Content-Encoding'] = encoding
1294 1294
1295 1295 web.res.setbodywillwrite()
1296 1296 if list(web.res.sendresponse()):
1297 1297 raise error.ProgrammingError(
1298 1298 b'sendresponse() should not emit data if writing later'
1299 1299 )
1300 1300
1301 1301 bodyfh = web.res.getbodyfile()
1302 1302
1303 1303 archival.archive(
1304 1304 web.repo,
1305 1305 bodyfh,
1306 1306 cnode,
1307 1307 artype,
1308 1308 prefix=name,
1309 1309 match=match,
1310 1310 subrepos=web.configbool(b"web", b"archivesubrepos"),
1311 1311 )
1312 1312
1313 1313 return []
1314 1314
1315 1315
1316 1316 @webcommand(b'static')
1317 1317 def static(web):
1318 1318 fname = web.req.qsparams[b'file']
1319 1319 # a repo owner may set web.static in .hg/hgrc to get any file
1320 1320 # readable by the user running the CGI script
1321 1321 static = web.config(b"web", b"static", untrusted=False)
1322 1322 if not static:
1323 1323 tp = web.templatepath or templater.templatepaths()
1324 if isinstance(tp, str):
1324 if isinstance(tp, bytes):
1325 1325 tp = [tp]
1326 1326 static = [os.path.join(p, b'static') for p in tp]
1327 1327
1328 1328 staticfile(static, fname, web.res)
1329 1329 return web.res.sendresponse()
1330 1330
1331 1331
1332 1332 @webcommand(b'graph')
1333 1333 def graph(web):
1334 1334 """
1335 1335 /graph[/{revision}]
1336 1336 -------------------
1337 1337
1338 1338 Show information about the graphical topology of the repository.
1339 1339
1340 1340 Information rendered by this handler can be used to create visual
1341 1341 representations of repository topology.
1342 1342
1343 1343 The ``revision`` URL parameter controls the starting changeset. If it's
1344 1344 absent, the default is ``tip``.
1345 1345
1346 1346 The ``revcount`` query string argument can define the number of changesets
1347 1347 to show information for.
1348 1348
1349 1349 The ``graphtop`` query string argument can specify the starting changeset
1350 1350 for producing ``jsdata`` variable that is used for rendering graph in
1351 1351 JavaScript. By default it has the same value as ``revision``.
1352 1352
1353 1353 This handler will render the ``graph`` template.
1354 1354 """
1355 1355
1356 1356 if b'node' in web.req.qsparams:
1357 1357 ctx = webutil.changectx(web.repo, web.req)
1358 1358 symrev = webutil.symrevorshortnode(web.req, ctx)
1359 1359 else:
1360 1360 ctx = web.repo[b'tip']
1361 1361 symrev = b'tip'
1362 1362 rev = ctx.rev()
1363 1363
1364 1364 bg_height = 39
1365 1365 revcount = web.maxshortchanges
1366 1366 if b'revcount' in web.req.qsparams:
1367 1367 try:
1368 1368 revcount = int(web.req.qsparams.get(b'revcount', revcount))
1369 1369 revcount = max(revcount, 1)
1370 1370 web.tmpl.defaults[b'sessionvars'][b'revcount'] = revcount
1371 1371 except ValueError:
1372 1372 pass
1373 1373
1374 1374 lessvars = copy.copy(web.tmpl.defaults[b'sessionvars'])
1375 1375 lessvars[b'revcount'] = max(revcount // 2, 1)
1376 1376 morevars = copy.copy(web.tmpl.defaults[b'sessionvars'])
1377 1377 morevars[b'revcount'] = revcount * 2
1378 1378
1379 1379 graphtop = web.req.qsparams.get(b'graphtop', ctx.hex())
1380 1380 graphvars = copy.copy(web.tmpl.defaults[b'sessionvars'])
1381 1381 graphvars[b'graphtop'] = graphtop
1382 1382
1383 1383 count = len(web.repo)
1384 1384 pos = rev
1385 1385
1386 1386 uprev = min(max(0, count - 1), rev + revcount)
1387 1387 downrev = max(0, rev - revcount)
1388 1388 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1389 1389
1390 1390 tree = []
1391 1391 nextentry = []
1392 1392 lastrev = 0
1393 1393 if pos != -1:
1394 1394 allrevs = web.repo.changelog.revs(pos, 0)
1395 1395 revs = []
1396 1396 for i in allrevs:
1397 1397 revs.append(i)
1398 1398 if len(revs) >= revcount + 1:
1399 1399 break
1400 1400
1401 1401 if len(revs) > revcount:
1402 1402 nextentry = [webutil.commonentry(web.repo, web.repo[revs[-1]])]
1403 1403 revs = revs[:-1]
1404 1404
1405 1405 lastrev = revs[-1]
1406 1406
1407 1407 # We have to feed a baseset to dagwalker as it is expecting smartset
1408 1408 # object. This does not have a big impact on hgweb performance itself
1409 1409 # since hgweb graphing code is not itself lazy yet.
1410 1410 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1411 1411 # As we said one line above... not lazy.
1412 1412 tree = list(
1413 1413 item
1414 1414 for item in graphmod.colored(dag, web.repo)
1415 1415 if item[1] == graphmod.CHANGESET
1416 1416 )
1417 1417
1418 1418 def fulltree():
1419 1419 pos = web.repo[graphtop].rev()
1420 1420 tree = []
1421 1421 if pos != -1:
1422 1422 revs = web.repo.changelog.revs(pos, lastrev)
1423 1423 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1424 1424 tree = list(
1425 1425 item
1426 1426 for item in graphmod.colored(dag, web.repo)
1427 1427 if item[1] == graphmod.CHANGESET
1428 1428 )
1429 1429 return tree
1430 1430
1431 1431 def jsdata(context):
1432 1432 for (id, type, ctx, vtx, edges) in fulltree():
1433 1433 yield {
1434 1434 b'node': pycompat.bytestr(ctx),
1435 1435 b'graphnode': webutil.getgraphnode(web.repo, ctx),
1436 1436 b'vertex': vtx,
1437 1437 b'edges': edges,
1438 1438 }
1439 1439
1440 1440 def nodes(context):
1441 1441 parity = paritygen(web.stripecount)
1442 1442 for row, (id, type, ctx, vtx, edges) in enumerate(tree):
1443 1443 entry = webutil.commonentry(web.repo, ctx)
1444 1444 edgedata = [
1445 1445 {
1446 1446 b'col': edge[0],
1447 1447 b'nextcol': edge[1],
1448 1448 b'color': (edge[2] - 1) % 6 + 1,
1449 1449 b'width': edge[3],
1450 1450 b'bcolor': edge[4],
1451 1451 }
1452 1452 for edge in edges
1453 1453 ]
1454 1454
1455 1455 entry.update(
1456 1456 {
1457 1457 b'col': vtx[0],
1458 1458 b'color': (vtx[1] - 1) % 6 + 1,
1459 1459 b'parity': next(parity),
1460 1460 b'edges': templateutil.mappinglist(edgedata),
1461 1461 b'row': row,
1462 1462 b'nextrow': row + 1,
1463 1463 }
1464 1464 )
1465 1465
1466 1466 yield entry
1467 1467
1468 1468 rows = len(tree)
1469 1469
1470 1470 return web.sendtemplate(
1471 1471 b'graph',
1472 1472 rev=rev,
1473 1473 symrev=symrev,
1474 1474 revcount=revcount,
1475 1475 uprev=uprev,
1476 1476 lessvars=lessvars,
1477 1477 morevars=morevars,
1478 1478 downrev=downrev,
1479 1479 graphvars=graphvars,
1480 1480 rows=rows,
1481 1481 bg_height=bg_height,
1482 1482 changesets=count,
1483 1483 nextentry=templateutil.mappinglist(nextentry),
1484 1484 jsdata=templateutil.mappinggenerator(jsdata),
1485 1485 nodes=templateutil.mappinggenerator(nodes),
1486 1486 node=ctx.hex(),
1487 1487 archives=web.archivelist(b'tip'),
1488 1488 changenav=changenav,
1489 1489 )
1490 1490
1491 1491
1492 1492 def _getdoc(e):
1493 1493 doc = e[0].__doc__
1494 1494 if doc:
1495 1495 doc = _(doc).partition(b'\n')[0]
1496 1496 else:
1497 1497 doc = _(b'(no help text available)')
1498 1498 return doc
1499 1499
1500 1500
1501 1501 @webcommand(b'help')
1502 1502 def help(web):
1503 1503 """
1504 1504 /help[/{topic}]
1505 1505 ---------------
1506 1506
1507 1507 Render help documentation.
1508 1508
1509 1509 This web command is roughly equivalent to :hg:`help`. If a ``topic``
1510 1510 is defined, that help topic will be rendered. If not, an index of
1511 1511 available help topics will be rendered.
1512 1512
1513 1513 The ``help`` template will be rendered when requesting help for a topic.
1514 1514 ``helptopics`` will be rendered for the index of help topics.
1515 1515 """
1516 1516 from .. import commands, help as helpmod # avoid cycle
1517 1517
1518 1518 topicname = web.req.qsparams.get(b'node')
1519 1519 if not topicname:
1520 1520
1521 1521 def topics(context):
1522 1522 for h in helpmod.helptable:
1523 1523 entries, summary, _doc = h[0:3]
1524 1524 yield {b'topic': entries[0], b'summary': summary}
1525 1525
1526 1526 early, other = [], []
1527 1527 primary = lambda s: s.partition(b'|')[0]
1528 1528 for c, e in pycompat.iteritems(commands.table):
1529 1529 doc = _getdoc(e)
1530 1530 if b'DEPRECATED' in doc or c.startswith(b'debug'):
1531 1531 continue
1532 1532 cmd = primary(c)
1533 1533 if getattr(e[0], 'helpbasic', False):
1534 1534 early.append((cmd, doc))
1535 1535 else:
1536 1536 other.append((cmd, doc))
1537 1537
1538 1538 early.sort()
1539 1539 other.sort()
1540 1540
1541 1541 def earlycommands(context):
1542 1542 for c, doc in early:
1543 1543 yield {b'topic': c, b'summary': doc}
1544 1544
1545 1545 def othercommands(context):
1546 1546 for c, doc in other:
1547 1547 yield {b'topic': c, b'summary': doc}
1548 1548
1549 1549 return web.sendtemplate(
1550 1550 b'helptopics',
1551 1551 topics=templateutil.mappinggenerator(topics),
1552 1552 earlycommands=templateutil.mappinggenerator(earlycommands),
1553 1553 othercommands=templateutil.mappinggenerator(othercommands),
1554 1554 title=b'Index',
1555 1555 )
1556 1556
1557 1557 # Render an index of sub-topics.
1558 1558 if topicname in helpmod.subtopics:
1559 1559 topics = []
1560 1560 for entries, summary, _doc in helpmod.subtopics[topicname]:
1561 1561 topics.append(
1562 1562 {
1563 1563 b'topic': b'%s.%s' % (topicname, entries[0]),
1564 1564 b'basename': entries[0],
1565 1565 b'summary': summary,
1566 1566 }
1567 1567 )
1568 1568
1569 1569 return web.sendtemplate(
1570 1570 b'helptopics',
1571 1571 topics=templateutil.mappinglist(topics),
1572 1572 title=topicname,
1573 1573 subindex=True,
1574 1574 )
1575 1575
1576 1576 u = webutil.wsgiui.load()
1577 1577 u.verbose = True
1578 1578
1579 1579 # Render a page from a sub-topic.
1580 1580 if b'.' in topicname:
1581 1581 # TODO implement support for rendering sections, like
1582 1582 # `hg help` works.
1583 1583 topic, subtopic = topicname.split(b'.', 1)
1584 1584 if topic not in helpmod.subtopics:
1585 1585 raise ErrorResponse(HTTP_NOT_FOUND)
1586 1586 else:
1587 1587 topic = topicname
1588 1588 subtopic = None
1589 1589
1590 1590 try:
1591 1591 doc = helpmod.help_(u, commands, topic, subtopic=subtopic)
1592 1592 except error.Abort:
1593 1593 raise ErrorResponse(HTTP_NOT_FOUND)
1594 1594
1595 1595 return web.sendtemplate(b'help', topic=topicname, doc=doc)
1596 1596
1597 1597
1598 1598 # tell hggettext to extract docstrings from these functions:
1599 1599 i18nfunctions = commands.values()
@@ -1,940 +1,940 b''
1 1 # hgweb/webutil.py - utility library for the web interface.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from __future__ import absolute_import
10 10
11 11 import copy
12 12 import difflib
13 13 import os
14 14 import re
15 15
16 16 from ..i18n import _
17 17 from ..node import hex, nullid, short
18 18 from ..pycompat import setattr
19 19
20 20 from .common import (
21 21 ErrorResponse,
22 22 HTTP_BAD_REQUEST,
23 23 HTTP_NOT_FOUND,
24 24 paritygen,
25 25 )
26 26
27 27 from .. import (
28 28 context,
29 29 diffutil,
30 30 error,
31 31 match,
32 32 mdiff,
33 33 obsutil,
34 34 patch,
35 35 pathutil,
36 36 pycompat,
37 37 scmutil,
38 38 templatefilters,
39 39 templatekw,
40 40 templateutil,
41 41 ui as uimod,
42 42 util,
43 43 )
44 44
45 45 from ..utils import stringutil
46 46
47 47 archivespecs = util.sortdict(
48 48 (
49 49 (b'zip', (b'application/zip', b'zip', b'.zip', None)),
50 50 (b'gz', (b'application/x-gzip', b'tgz', b'.tar.gz', None)),
51 51 (b'bz2', (b'application/x-bzip2', b'tbz2', b'.tar.bz2', None)),
52 52 )
53 53 )
54 54
55 55
56 56 def archivelist(ui, nodeid, url=None):
57 57 allowed = ui.configlist(b'web', b'allow-archive', untrusted=True)
58 58 archives = []
59 59
60 60 for typ, spec in pycompat.iteritems(archivespecs):
61 61 if typ in allowed or ui.configbool(
62 62 b'web', b'allow' + typ, untrusted=True
63 63 ):
64 64 archives.append(
65 65 {
66 66 b'type': typ,
67 67 b'extension': spec[2],
68 68 b'node': nodeid,
69 69 b'url': url,
70 70 }
71 71 )
72 72
73 73 return templateutil.mappinglist(archives)
74 74
75 75
76 76 def up(p):
77 77 if p[0:1] != b"/":
78 78 p = b"/" + p
79 79 if p[-1:] == b"/":
80 80 p = p[:-1]
81 81 up = os.path.dirname(p)
82 82 if up == b"/":
83 83 return b"/"
84 84 return up + b"/"
85 85
86 86
87 87 def _navseq(step, firststep=None):
88 88 if firststep:
89 89 yield firststep
90 90 if firststep >= 20 and firststep <= 40:
91 91 firststep = 50
92 92 yield firststep
93 93 assert step > 0
94 94 assert firststep > 0
95 95 while step <= firststep:
96 96 step *= 10
97 97 while True:
98 98 yield 1 * step
99 99 yield 3 * step
100 100 step *= 10
101 101
102 102
103 103 class revnav(object):
104 104 def __init__(self, repo):
105 105 """Navigation generation object
106 106
107 107 :repo: repo object we generate nav for
108 108 """
109 109 # used for hex generation
110 110 self._revlog = repo.changelog
111 111
112 112 def __nonzero__(self):
113 113 """return True if any revision to navigate over"""
114 114 return self._first() is not None
115 115
116 116 __bool__ = __nonzero__
117 117
118 118 def _first(self):
119 119 """return the minimum non-filtered changeset or None"""
120 120 try:
121 121 return next(iter(self._revlog))
122 122 except StopIteration:
123 123 return None
124 124
125 125 def hex(self, rev):
126 126 return hex(self._revlog.node(rev))
127 127
128 128 def gen(self, pos, pagelen, limit):
129 129 """computes label and revision id for navigation link
130 130
131 131 :pos: is the revision relative to which we generate navigation.
132 132 :pagelen: the size of each navigation page
133 133 :limit: how far shall we link
134 134
135 135 The return is:
136 136 - a single element mappinglist
137 137 - containing a dictionary with a `before` and `after` key
138 138 - values are dictionaries with `label` and `node` keys
139 139 """
140 140 if not self:
141 141 # empty repo
142 142 return templateutil.mappinglist(
143 143 [
144 144 {
145 145 b'before': templateutil.mappinglist([]),
146 146 b'after': templateutil.mappinglist([]),
147 147 },
148 148 ]
149 149 )
150 150
151 151 targets = []
152 152 for f in _navseq(1, pagelen):
153 153 if f > limit:
154 154 break
155 155 targets.append(pos + f)
156 156 targets.append(pos - f)
157 157 targets.sort()
158 158
159 159 first = self._first()
160 160 navbefore = [{b'label': b'(%i)' % first, b'node': self.hex(first)}]
161 161 navafter = []
162 162 for rev in targets:
163 163 if rev not in self._revlog:
164 164 continue
165 165 if pos < rev < limit:
166 166 navafter.append(
167 167 {b'label': b'+%d' % abs(rev - pos), b'node': self.hex(rev)}
168 168 )
169 169 if 0 < rev < pos:
170 170 navbefore.append(
171 171 {b'label': b'-%d' % abs(rev - pos), b'node': self.hex(rev)}
172 172 )
173 173
174 174 navafter.append({b'label': b'tip', b'node': b'tip'})
175 175
176 176 # TODO: maybe this can be a scalar object supporting tomap()
177 177 return templateutil.mappinglist(
178 178 [
179 179 {
180 180 b'before': templateutil.mappinglist(navbefore),
181 181 b'after': templateutil.mappinglist(navafter),
182 182 },
183 183 ]
184 184 )
185 185
186 186
187 187 class filerevnav(revnav):
188 188 def __init__(self, repo, path):
189 189 """Navigation generation object
190 190
191 191 :repo: repo object we generate nav for
192 192 :path: path of the file we generate nav for
193 193 """
194 194 # used for iteration
195 195 self._changelog = repo.unfiltered().changelog
196 196 # used for hex generation
197 197 self._revlog = repo.file(path)
198 198
199 199 def hex(self, rev):
200 200 return hex(self._changelog.node(self._revlog.linkrev(rev)))
201 201
202 202
203 203 # TODO: maybe this can be a wrapper class for changectx/filectx list, which
204 204 # yields {'ctx': ctx}
205 205 def _ctxsgen(context, ctxs):
206 206 for s in ctxs:
207 207 d = {
208 208 b'node': s.hex(),
209 209 b'rev': s.rev(),
210 210 b'user': s.user(),
211 211 b'date': s.date(),
212 212 b'description': s.description(),
213 213 b'branch': s.branch(),
214 214 }
215 215 if util.safehasattr(s, b'path'):
216 216 d[b'file'] = s.path()
217 217 yield d
218 218
219 219
220 220 def _siblings(siblings=None, hiderev=None):
221 221 if siblings is None:
222 222 siblings = []
223 223 siblings = [s for s in siblings if s.node() != nullid]
224 224 if len(siblings) == 1 and siblings[0].rev() == hiderev:
225 225 siblings = []
226 226 return templateutil.mappinggenerator(_ctxsgen, args=(siblings,))
227 227
228 228
229 229 def difffeatureopts(req, ui, section):
230 230 diffopts = diffutil.difffeatureopts(
231 231 ui, untrusted=True, section=section, whitespace=True
232 232 )
233 233
234 234 for k in (
235 235 b'ignorews',
236 236 b'ignorewsamount',
237 237 b'ignorewseol',
238 238 b'ignoreblanklines',
239 239 ):
240 240 v = req.qsparams.get(k)
241 241 if v is not None:
242 242 v = stringutil.parsebool(v)
243 243 setattr(diffopts, k, v if v is not None else True)
244 244
245 245 return diffopts
246 246
247 247
248 248 def annotate(req, fctx, ui):
249 249 diffopts = difffeatureopts(req, ui, b'annotate')
250 250 return fctx.annotate(follow=True, diffopts=diffopts)
251 251
252 252
253 253 def parents(ctx, hide=None):
254 254 if isinstance(ctx, context.basefilectx):
255 255 introrev = ctx.introrev()
256 256 if ctx.changectx().rev() != introrev:
257 257 return _siblings([ctx.repo()[introrev]], hide)
258 258 return _siblings(ctx.parents(), hide)
259 259
260 260
261 261 def children(ctx, hide=None):
262 262 return _siblings(ctx.children(), hide)
263 263
264 264
265 265 def renamelink(fctx):
266 266 r = fctx.renamed()
267 267 if r:
268 268 return templateutil.mappinglist([{b'file': r[0], b'node': hex(r[1])}])
269 269 return templateutil.mappinglist([])
270 270
271 271
272 272 def nodetagsdict(repo, node):
273 273 return templateutil.hybridlist(repo.nodetags(node), name=b'name')
274 274
275 275
276 276 def nodebookmarksdict(repo, node):
277 277 return templateutil.hybridlist(repo.nodebookmarks(node), name=b'name')
278 278
279 279
280 280 def nodebranchdict(repo, ctx):
281 281 branches = []
282 282 branch = ctx.branch()
283 283 # If this is an empty repo, ctx.node() == nullid,
284 284 # ctx.branch() == 'default'.
285 285 try:
286 286 branchnode = repo.branchtip(branch)
287 287 except error.RepoLookupError:
288 288 branchnode = None
289 289 if branchnode == ctx.node():
290 290 branches.append(branch)
291 291 return templateutil.hybridlist(branches, name=b'name')
292 292
293 293
294 294 def nodeinbranch(repo, ctx):
295 295 branches = []
296 296 branch = ctx.branch()
297 297 try:
298 298 branchnode = repo.branchtip(branch)
299 299 except error.RepoLookupError:
300 300 branchnode = None
301 301 if branch != b'default' and branchnode != ctx.node():
302 302 branches.append(branch)
303 303 return templateutil.hybridlist(branches, name=b'name')
304 304
305 305
306 306 def nodebranchnodefault(ctx):
307 307 branches = []
308 308 branch = ctx.branch()
309 309 if branch != b'default':
310 310 branches.append(branch)
311 311 return templateutil.hybridlist(branches, name=b'name')
312 312
313 313
314 314 def _nodenamesgen(context, f, node, name):
315 315 for t in f(node):
316 316 yield {name: t}
317 317
318 318
319 319 def showtag(repo, t1, node=nullid):
320 320 args = (repo.nodetags, node, b'tag')
321 321 return templateutil.mappinggenerator(_nodenamesgen, args=args, name=t1)
322 322
323 323
324 324 def showbookmark(repo, t1, node=nullid):
325 325 args = (repo.nodebookmarks, node, b'bookmark')
326 326 return templateutil.mappinggenerator(_nodenamesgen, args=args, name=t1)
327 327
328 328
329 329 def branchentries(repo, stripecount, limit=0):
330 330 tips = []
331 331 heads = repo.heads()
332 332 parity = paritygen(stripecount)
333 333 sortkey = lambda item: (not item[1], item[0].rev())
334 334
335 335 def entries(context):
336 336 count = 0
337 337 if not tips:
338 338 for tag, hs, tip, closed in repo.branchmap().iterbranches():
339 339 tips.append((repo[tip], closed))
340 340 for ctx, closed in sorted(tips, key=sortkey, reverse=True):
341 341 if limit > 0 and count >= limit:
342 342 return
343 343 count += 1
344 344 if closed:
345 345 status = b'closed'
346 346 elif ctx.node() not in heads:
347 347 status = b'inactive'
348 348 else:
349 349 status = b'open'
350 350 yield {
351 351 b'parity': next(parity),
352 352 b'branch': ctx.branch(),
353 353 b'status': status,
354 354 b'node': ctx.hex(),
355 355 b'date': ctx.date(),
356 356 }
357 357
358 358 return templateutil.mappinggenerator(entries)
359 359
360 360
361 361 def cleanpath(repo, path):
362 362 path = path.lstrip(b'/')
363 363 auditor = pathutil.pathauditor(repo.root, realfs=False)
364 364 return pathutil.canonpath(repo.root, b'', path, auditor=auditor)
365 365
366 366
367 367 def changectx(repo, req):
368 368 changeid = b"tip"
369 369 if b'node' in req.qsparams:
370 370 changeid = req.qsparams[b'node']
371 371 ipos = changeid.find(b':')
372 372 if ipos != -1:
373 373 changeid = changeid[(ipos + 1) :]
374 374
375 375 return scmutil.revsymbol(repo, changeid)
376 376
377 377
378 378 def basechangectx(repo, req):
379 379 if b'node' in req.qsparams:
380 380 changeid = req.qsparams[b'node']
381 381 ipos = changeid.find(b':')
382 382 if ipos != -1:
383 383 changeid = changeid[:ipos]
384 384 return scmutil.revsymbol(repo, changeid)
385 385
386 386 return None
387 387
388 388
389 389 def filectx(repo, req):
390 390 if b'file' not in req.qsparams:
391 391 raise ErrorResponse(HTTP_NOT_FOUND, b'file not given')
392 392 path = cleanpath(repo, req.qsparams[b'file'])
393 393 if b'node' in req.qsparams:
394 394 changeid = req.qsparams[b'node']
395 395 elif b'filenode' in req.qsparams:
396 396 changeid = req.qsparams[b'filenode']
397 397 else:
398 398 raise ErrorResponse(HTTP_NOT_FOUND, b'node or filenode not given')
399 399 try:
400 400 fctx = scmutil.revsymbol(repo, changeid)[path]
401 401 except error.RepoError:
402 402 fctx = repo.filectx(path, fileid=changeid)
403 403
404 404 return fctx
405 405
406 406
407 407 def linerange(req):
408 408 linerange = req.qsparams.getall(b'linerange')
409 409 if not linerange:
410 410 return None
411 411 if len(linerange) > 1:
412 412 raise ErrorResponse(HTTP_BAD_REQUEST, b'redundant linerange parameter')
413 413 try:
414 414 fromline, toline = map(int, linerange[0].split(b':', 1))
415 415 except ValueError:
416 416 raise ErrorResponse(HTTP_BAD_REQUEST, b'invalid linerange parameter')
417 417 try:
418 418 return util.processlinerange(fromline, toline)
419 419 except error.ParseError as exc:
420 420 raise ErrorResponse(HTTP_BAD_REQUEST, pycompat.bytestr(exc))
421 421
422 422
423 423 def formatlinerange(fromline, toline):
424 424 return b'%d:%d' % (fromline + 1, toline)
425 425
426 426
427 427 def _succsandmarkersgen(context, mapping):
428 428 repo = context.resource(mapping, b'repo')
429 429 itemmappings = templatekw.showsuccsandmarkers(context, mapping)
430 430 for item in itemmappings.tovalue(context, mapping):
431 431 item[b'successors'] = _siblings(
432 432 repo[successor] for successor in item[b'successors']
433 433 )
434 434 yield item
435 435
436 436
437 437 def succsandmarkers(context, mapping):
438 438 return templateutil.mappinggenerator(_succsandmarkersgen, args=(mapping,))
439 439
440 440
441 441 # teach templater succsandmarkers is switched to (context, mapping) API
442 442 succsandmarkers._requires = {b'repo', b'ctx'}
443 443
444 444
445 445 def _whyunstablegen(context, mapping):
446 446 repo = context.resource(mapping, b'repo')
447 447 ctx = context.resource(mapping, b'ctx')
448 448
449 449 entries = obsutil.whyunstable(repo, ctx)
450 450 for entry in entries:
451 451 if entry.get(b'divergentnodes'):
452 452 entry[b'divergentnodes'] = _siblings(entry[b'divergentnodes'])
453 453 yield entry
454 454
455 455
456 456 def whyunstable(context, mapping):
457 457 return templateutil.mappinggenerator(_whyunstablegen, args=(mapping,))
458 458
459 459
460 460 whyunstable._requires = {b'repo', b'ctx'}
461 461
462 462
463 463 def commonentry(repo, ctx):
464 464 node = scmutil.binnode(ctx)
465 465 return {
466 466 # TODO: perhaps ctx.changectx() should be assigned if ctx is a
467 467 # filectx, but I'm not pretty sure if that would always work because
468 468 # fctx.parents() != fctx.changectx.parents() for example.
469 469 b'ctx': ctx,
470 470 b'rev': ctx.rev(),
471 471 b'node': hex(node),
472 472 b'author': ctx.user(),
473 473 b'desc': ctx.description(),
474 474 b'date': ctx.date(),
475 475 b'extra': ctx.extra(),
476 476 b'phase': ctx.phasestr(),
477 477 b'obsolete': ctx.obsolete(),
478 478 b'succsandmarkers': succsandmarkers,
479 479 b'instabilities': templateutil.hybridlist(
480 480 ctx.instabilities(), name=b'instability'
481 481 ),
482 482 b'whyunstable': whyunstable,
483 483 b'branch': nodebranchnodefault(ctx),
484 484 b'inbranch': nodeinbranch(repo, ctx),
485 485 b'branches': nodebranchdict(repo, ctx),
486 486 b'tags': nodetagsdict(repo, node),
487 487 b'bookmarks': nodebookmarksdict(repo, node),
488 488 b'parent': lambda context, mapping: parents(ctx),
489 489 b'child': lambda context, mapping: children(ctx),
490 490 }
491 491
492 492
493 493 def changelistentry(web, ctx):
494 494 '''Obtain a dictionary to be used for entries in a changelist.
495 495
496 496 This function is called when producing items for the "entries" list passed
497 497 to the "shortlog" and "changelog" templates.
498 498 '''
499 499 repo = web.repo
500 500 rev = ctx.rev()
501 501 n = scmutil.binnode(ctx)
502 502 showtags = showtag(repo, b'changelogtag', n)
503 503 files = listfilediffs(ctx.files(), n, web.maxfiles)
504 504
505 505 entry = commonentry(repo, ctx)
506 506 entry.update(
507 507 {
508 508 b'allparents': lambda context, mapping: parents(ctx),
509 509 b'parent': lambda context, mapping: parents(ctx, rev - 1),
510 510 b'child': lambda context, mapping: children(ctx, rev + 1),
511 511 b'changelogtag': showtags,
512 512 b'files': files,
513 513 }
514 514 )
515 515 return entry
516 516
517 517
518 518 def changelistentries(web, revs, maxcount, parityfn):
519 519 """Emit up to N records for an iterable of revisions."""
520 520 repo = web.repo
521 521
522 522 count = 0
523 523 for rev in revs:
524 524 if count >= maxcount:
525 525 break
526 526
527 527 count += 1
528 528
529 529 entry = changelistentry(web, repo[rev])
530 530 entry[b'parity'] = next(parityfn)
531 531
532 532 yield entry
533 533
534 534
535 535 def symrevorshortnode(req, ctx):
536 536 if b'node' in req.qsparams:
537 537 return templatefilters.revescape(req.qsparams[b'node'])
538 538 else:
539 539 return short(scmutil.binnode(ctx))
540 540
541 541
542 542 def _listfilesgen(context, ctx, stripecount):
543 543 parity = paritygen(stripecount)
544 544 filesadded = ctx.filesadded()
545 545 for blockno, f in enumerate(ctx.files()):
546 546 if f not in ctx:
547 547 status = b'removed'
548 548 elif f in filesadded:
549 549 status = b'added'
550 550 else:
551 551 status = b'modified'
552 552 template = b'filenolink' if status == b'removed' else b'filenodelink'
553 553 yield context.process(
554 554 template,
555 555 {
556 556 b'node': ctx.hex(),
557 557 b'file': f,
558 558 b'blockno': blockno + 1,
559 559 b'parity': next(parity),
560 560 b'status': status,
561 561 },
562 562 )
563 563
564 564
565 565 def changesetentry(web, ctx):
566 566 '''Obtain a dictionary to be used to render the "changeset" template.'''
567 567
568 568 showtags = showtag(web.repo, b'changesettag', scmutil.binnode(ctx))
569 569 showbookmarks = showbookmark(
570 570 web.repo, b'changesetbookmark', scmutil.binnode(ctx)
571 571 )
572 572 showbranch = nodebranchnodefault(ctx)
573 573
574 574 basectx = basechangectx(web.repo, web.req)
575 575 if basectx is None:
576 576 basectx = ctx.p1()
577 577
578 578 style = web.config(b'web', b'style')
579 579 if b'style' in web.req.qsparams:
580 580 style = web.req.qsparams[b'style']
581 581
582 582 diff = diffs(web, ctx, basectx, None, style)
583 583
584 584 parity = paritygen(web.stripecount)
585 585 diffstatsgen = diffstatgen(web.repo.ui, ctx, basectx)
586 586 diffstats = diffstat(ctx, diffstatsgen, parity)
587 587
588 588 return dict(
589 589 diff=diff,
590 590 symrev=symrevorshortnode(web.req, ctx),
591 591 basenode=basectx.hex(),
592 592 changesettag=showtags,
593 593 changesetbookmark=showbookmarks,
594 594 changesetbranch=showbranch,
595 595 files=templateutil.mappedgenerator(
596 596 _listfilesgen, args=(ctx, web.stripecount)
597 597 ),
598 598 diffsummary=lambda context, mapping: diffsummary(diffstatsgen),
599 599 diffstat=diffstats,
600 600 archives=web.archivelist(ctx.hex()),
601 601 **pycompat.strkwargs(commonentry(web.repo, ctx))
602 602 )
603 603
604 604
605 605 def _listfilediffsgen(context, files, node, max):
606 606 for f in files[:max]:
607 607 yield context.process(b'filedifflink', {b'node': hex(node), b'file': f})
608 608 if len(files) > max:
609 609 yield context.process(b'fileellipses', {})
610 610
611 611
612 612 def listfilediffs(files, node, max):
613 613 return templateutil.mappedgenerator(
614 614 _listfilediffsgen, args=(files, node, max)
615 615 )
616 616
617 617
618 618 def _prettyprintdifflines(context, lines, blockno, lineidprefix):
619 619 for lineno, l in enumerate(lines, 1):
620 620 difflineno = b"%d.%d" % (blockno, lineno)
621 621 if l.startswith(b'+'):
622 622 ltype = b"difflineplus"
623 623 elif l.startswith(b'-'):
624 624 ltype = b"difflineminus"
625 625 elif l.startswith(b'@'):
626 626 ltype = b"difflineat"
627 627 else:
628 628 ltype = b"diffline"
629 629 yield context.process(
630 630 ltype,
631 631 {
632 632 b'line': l,
633 633 b'lineno': lineno,
634 634 b'lineid': lineidprefix + b"l%s" % difflineno,
635 635 b'linenumber': b"% 8s" % difflineno,
636 636 },
637 637 )
638 638
639 639
640 640 def _diffsgen(
641 641 context,
642 642 repo,
643 643 ctx,
644 644 basectx,
645 645 files,
646 646 style,
647 647 stripecount,
648 648 linerange,
649 649 lineidprefix,
650 650 ):
651 651 if files:
652 652 m = match.exact(files)
653 653 else:
654 654 m = match.always()
655 655
656 656 diffopts = patch.diffopts(repo.ui, untrusted=True)
657 657 parity = paritygen(stripecount)
658 658
659 659 diffhunks = patch.diffhunks(repo, basectx, ctx, m, opts=diffopts)
660 660 for blockno, (fctx1, fctx2, header, hunks) in enumerate(diffhunks, 1):
661 661 if style != b'raw':
662 662 header = header[1:]
663 663 lines = [h + b'\n' for h in header]
664 664 for hunkrange, hunklines in hunks:
665 665 if linerange is not None and hunkrange is not None:
666 666 s1, l1, s2, l2 = hunkrange
667 667 if not mdiff.hunkinrange((s2, l2), linerange):
668 668 continue
669 669 lines.extend(hunklines)
670 670 if lines:
671 671 l = templateutil.mappedgenerator(
672 672 _prettyprintdifflines, args=(lines, blockno, lineidprefix)
673 673 )
674 674 yield {
675 675 b'parity': next(parity),
676 676 b'blockno': blockno,
677 677 b'lines': l,
678 678 }
679 679
680 680
681 681 def diffs(web, ctx, basectx, files, style, linerange=None, lineidprefix=b''):
682 682 args = (
683 683 web.repo,
684 684 ctx,
685 685 basectx,
686 686 files,
687 687 style,
688 688 web.stripecount,
689 689 linerange,
690 690 lineidprefix,
691 691 )
692 692 return templateutil.mappinggenerator(
693 693 _diffsgen, args=args, name=b'diffblock'
694 694 )
695 695
696 696
697 697 def _compline(type, leftlineno, leftline, rightlineno, rightline):
698 698 lineid = leftlineno and (b"l%d" % leftlineno) or b''
699 699 lineid += rightlineno and (b"r%d" % rightlineno) or b''
700 700 llno = b'%d' % leftlineno if leftlineno else b''
701 701 rlno = b'%d' % rightlineno if rightlineno else b''
702 702 return {
703 703 b'type': type,
704 704 b'lineid': lineid,
705 705 b'leftlineno': leftlineno,
706 706 b'leftlinenumber': b"% 6s" % llno,
707 707 b'leftline': leftline or b'',
708 708 b'rightlineno': rightlineno,
709 709 b'rightlinenumber': b"% 6s" % rlno,
710 710 b'rightline': rightline or b'',
711 711 }
712 712
713 713
714 714 def _getcompblockgen(context, leftlines, rightlines, opcodes):
715 715 for type, llo, lhi, rlo, rhi in opcodes:
716 716 type = pycompat.sysbytes(type)
717 717 len1 = lhi - llo
718 718 len2 = rhi - rlo
719 719 count = min(len1, len2)
720 720 for i in pycompat.xrange(count):
721 721 yield _compline(
722 722 type=type,
723 723 leftlineno=llo + i + 1,
724 724 leftline=leftlines[llo + i],
725 725 rightlineno=rlo + i + 1,
726 726 rightline=rightlines[rlo + i],
727 727 )
728 728 if len1 > len2:
729 729 for i in pycompat.xrange(llo + count, lhi):
730 730 yield _compline(
731 731 type=type,
732 732 leftlineno=i + 1,
733 733 leftline=leftlines[i],
734 734 rightlineno=None,
735 735 rightline=None,
736 736 )
737 737 elif len2 > len1:
738 738 for i in pycompat.xrange(rlo + count, rhi):
739 739 yield _compline(
740 740 type=type,
741 741 leftlineno=None,
742 742 leftline=None,
743 743 rightlineno=i + 1,
744 744 rightline=rightlines[i],
745 745 )
746 746
747 747
748 748 def _getcompblock(leftlines, rightlines, opcodes):
749 749 args = (leftlines, rightlines, opcodes)
750 750 return templateutil.mappinggenerator(
751 751 _getcompblockgen, args=args, name=b'comparisonline'
752 752 )
753 753
754 754
755 755 def _comparegen(context, contextnum, leftlines, rightlines):
756 756 '''Generator function that provides side-by-side comparison data.'''
757 757 s = difflib.SequenceMatcher(None, leftlines, rightlines)
758 758 if contextnum < 0:
759 759 l = _getcompblock(leftlines, rightlines, s.get_opcodes())
760 760 yield {b'lines': l}
761 761 else:
762 762 for oc in s.get_grouped_opcodes(n=contextnum):
763 763 l = _getcompblock(leftlines, rightlines, oc)
764 764 yield {b'lines': l}
765 765
766 766
767 767 def compare(contextnum, leftlines, rightlines):
768 768 args = (contextnum, leftlines, rightlines)
769 769 return templateutil.mappinggenerator(
770 770 _comparegen, args=args, name=b'comparisonblock'
771 771 )
772 772
773 773
774 774 def diffstatgen(ui, ctx, basectx):
775 775 '''Generator function that provides the diffstat data.'''
776 776
777 777 diffopts = patch.diffopts(ui, {b'noprefix': False})
778 778 stats = patch.diffstatdata(util.iterlines(ctx.diff(basectx, opts=diffopts)))
779 779 maxname, maxtotal, addtotal, removetotal, binary = patch.diffstatsum(stats)
780 780 while True:
781 781 yield stats, maxname, maxtotal, addtotal, removetotal, binary
782 782
783 783
784 784 def diffsummary(statgen):
785 785 '''Return a short summary of the diff.'''
786 786
787 787 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
788 788 return _(b' %d files changed, %d insertions(+), %d deletions(-)\n') % (
789 789 len(stats),
790 790 addtotal,
791 791 removetotal,
792 792 )
793 793
794 794
795 795 def _diffstattmplgen(context, ctx, statgen, parity):
796 796 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
797 797 files = ctx.files()
798 798
799 799 def pct(i):
800 800 if maxtotal == 0:
801 801 return 0
802 802 return (float(i) / maxtotal) * 100
803 803
804 804 fileno = 0
805 805 for filename, adds, removes, isbinary in stats:
806 806 template = b'diffstatlink' if filename in files else b'diffstatnolink'
807 807 total = adds + removes
808 808 fileno += 1
809 809 yield context.process(
810 810 template,
811 811 {
812 812 b'node': ctx.hex(),
813 813 b'file': filename,
814 814 b'fileno': fileno,
815 815 b'total': total,
816 816 b'addpct': pct(adds),
817 817 b'removepct': pct(removes),
818 818 b'parity': next(parity),
819 819 },
820 820 )
821 821
822 822
823 823 def diffstat(ctx, statgen, parity):
824 824 '''Return a diffstat template for each file in the diff.'''
825 825 args = (ctx, statgen, parity)
826 826 return templateutil.mappedgenerator(_diffstattmplgen, args=args)
827 827
828 828
829 829 class sessionvars(templateutil.wrapped):
830 830 def __init__(self, vars, start=b'?'):
831 831 self._start = start
832 832 self._vars = vars
833 833
834 834 def __getitem__(self, key):
835 835 return self._vars[key]
836 836
837 837 def __setitem__(self, key, value):
838 838 self._vars[key] = value
839 839
840 840 def __copy__(self):
841 841 return sessionvars(copy.copy(self._vars), self._start)
842 842
843 843 def contains(self, context, mapping, item):
844 844 item = templateutil.unwrapvalue(context, mapping, item)
845 845 return item in self._vars
846 846
847 847 def getmember(self, context, mapping, key):
848 848 key = templateutil.unwrapvalue(context, mapping, key)
849 849 return self._vars.get(key)
850 850
851 851 def getmin(self, context, mapping):
852 852 raise error.ParseError(_(b'not comparable'))
853 853
854 854 def getmax(self, context, mapping):
855 855 raise error.ParseError(_(b'not comparable'))
856 856
857 857 def filter(self, context, mapping, select):
858 858 # implement if necessary
859 859 raise error.ParseError(_(b'not filterable'))
860 860
861 861 def itermaps(self, context):
862 862 separator = self._start
863 863 for key, value in sorted(pycompat.iteritems(self._vars)):
864 864 yield {
865 865 b'name': key,
866 866 b'value': pycompat.bytestr(value),
867 867 b'separator': separator,
868 868 }
869 869 separator = b'&'
870 870
871 871 def join(self, context, mapping, sep):
872 872 # could be '{separator}{name}={value|urlescape}'
873 873 raise error.ParseError(_(b'not displayable without template'))
874 874
875 875 def show(self, context, mapping):
876 return self.join(context, b'')
876 return self.join(context, mapping, b'')
877 877
878 878 def tobool(self, context, mapping):
879 879 return bool(self._vars)
880 880
881 881 def tovalue(self, context, mapping):
882 882 return self._vars
883 883
884 884
885 885 class wsgiui(uimod.ui):
886 886 # default termwidth breaks under mod_wsgi
887 887 def termwidth(self):
888 888 return 80
889 889
890 890
891 891 def getwebsubs(repo):
892 892 websubtable = []
893 893 websubdefs = repo.ui.configitems(b'websub')
894 894 # we must maintain interhg backwards compatibility
895 895 websubdefs += repo.ui.configitems(b'interhg')
896 896 for key, pattern in websubdefs:
897 897 # grab the delimiter from the character after the "s"
898 898 unesc = pattern[1:2]
899 899 delim = stringutil.reescape(unesc)
900 900
901 901 # identify portions of the pattern, taking care to avoid escaped
902 902 # delimiters. the replace format and flags are optional, but
903 903 # delimiters are required.
904 904 match = re.match(
905 905 br'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
906 906 % (delim, delim, delim),
907 907 pattern,
908 908 )
909 909 if not match:
910 910 repo.ui.warn(
911 911 _(b"websub: invalid pattern for %s: %s\n") % (key, pattern)
912 912 )
913 913 continue
914 914
915 915 # we need to unescape the delimiter for regexp and format
916 916 delim_re = re.compile(br'(?<!\\)\\%s' % delim)
917 917 regexp = delim_re.sub(unesc, match.group(1))
918 918 format = delim_re.sub(unesc, match.group(2))
919 919
920 920 # the pattern allows for 6 regexp flags, so set them if necessary
921 921 flagin = match.group(3)
922 922 flags = 0
923 923 if flagin:
924 924 for flag in pycompat.sysstr(flagin.upper()):
925 925 flags |= re.__dict__[flag]
926 926
927 927 try:
928 928 regexp = re.compile(regexp, flags)
929 929 websubtable.append((regexp, format))
930 930 except re.error:
931 931 repo.ui.warn(
932 932 _(b"websub: invalid regexp for %s: %s\n") % (key, regexp)
933 933 )
934 934 return websubtable
935 935
936 936
937 937 def getgraphnode(repo, ctx):
938 938 return templatekw.getgraphnodecurrent(
939 939 repo, ctx
940 940 ) + templatekw.getgraphnodesymbol(ctx)
@@ -1,1085 +1,1087 b''
1 1 # logcmdutil.py - utility for log-like commands
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import itertools
11 11 import os
12 12 import posixpath
13 13
14 14 from .i18n import _
15 15 from .node import (
16 16 nullid,
17 17 wdirid,
18 18 wdirrev,
19 19 )
20 20
21 21 from . import (
22 22 dagop,
23 23 error,
24 24 formatter,
25 25 graphmod,
26 26 match as matchmod,
27 27 mdiff,
28 28 patch,
29 29 pathutil,
30 30 pycompat,
31 31 revset,
32 32 revsetlang,
33 33 scmutil,
34 34 smartset,
35 35 templatekw,
36 36 templater,
37 37 util,
38 38 )
39 39 from .utils import (
40 40 dateutil,
41 41 stringutil,
42 42 )
43 43
44 44
45 45 if pycompat.TYPE_CHECKING:
46 46 from typing import (
47 47 Any,
48 48 Optional,
49 49 Tuple,
50 50 )
51 51
52 52 for t in (Any, Optional, Tuple):
53 53 assert t
54 54
55 55
56 56 def getlimit(opts):
57 57 """get the log limit according to option -l/--limit"""
58 58 limit = opts.get(b'limit')
59 59 if limit:
60 60 try:
61 61 limit = int(limit)
62 62 except ValueError:
63 63 raise error.Abort(_(b'limit must be a positive integer'))
64 64 if limit <= 0:
65 65 raise error.Abort(_(b'limit must be positive'))
66 66 else:
67 67 limit = None
68 68 return limit
69 69
70 70
71 71 def diffordiffstat(
72 72 ui,
73 73 repo,
74 74 diffopts,
75 75 node1,
76 76 node2,
77 77 match,
78 78 changes=None,
79 79 stat=False,
80 80 fp=None,
81 81 graphwidth=0,
82 82 prefix=b'',
83 83 root=b'',
84 84 listsubrepos=False,
85 85 hunksfilterfn=None,
86 86 ):
87 87 '''show diff or diffstat.'''
88 88 ctx1 = repo[node1]
89 89 ctx2 = repo[node2]
90 90 if root:
91 91 relroot = pathutil.canonpath(repo.root, repo.getcwd(), root)
92 92 else:
93 93 relroot = b''
94 94 copysourcematch = None
95 95
96 96 def compose(f, g):
97 97 return lambda x: f(g(x))
98 98
99 99 def pathfn(f):
100 100 return posixpath.join(prefix, f)
101 101
102 102 if relroot != b'':
103 103 # XXX relative roots currently don't work if the root is within a
104 104 # subrepo
105 105 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
106 106 uirelroot = uipathfn(pathfn(relroot))
107 107 relroot += b'/'
108 108 for matchroot in match.files():
109 109 if not matchroot.startswith(relroot):
110 110 ui.warn(
111 111 _(b'warning: %s not inside relative root %s\n')
112 112 % (uipathfn(pathfn(matchroot)), uirelroot)
113 113 )
114 114
115 115 relrootmatch = scmutil.match(ctx2, pats=[relroot], default=b'path')
116 116 match = matchmod.intersectmatchers(match, relrootmatch)
117 117 copysourcematch = relrootmatch
118 118
119 119 checkroot = repo.ui.configbool(
120 120 b'devel', b'all-warnings'
121 121 ) or repo.ui.configbool(b'devel', b'check-relroot')
122 122
123 123 def relrootpathfn(f):
124 124 if checkroot and not f.startswith(relroot):
125 125 raise AssertionError(
126 126 b"file %s doesn't start with relroot %s" % (f, relroot)
127 127 )
128 128 return f[len(relroot) :]
129 129
130 130 pathfn = compose(relrootpathfn, pathfn)
131 131
132 132 if stat:
133 133 diffopts = diffopts.copy(context=0, noprefix=False)
134 134 width = 80
135 135 if not ui.plain():
136 136 width = ui.termwidth() - graphwidth
137 137 # If an explicit --root was given, don't respect ui.relative-paths
138 138 if not relroot:
139 139 pathfn = compose(scmutil.getuipathfn(repo), pathfn)
140 140
141 141 chunks = ctx2.diff(
142 142 ctx1,
143 143 match,
144 144 changes,
145 145 opts=diffopts,
146 146 pathfn=pathfn,
147 147 copysourcematch=copysourcematch,
148 148 hunksfilterfn=hunksfilterfn,
149 149 )
150 150
151 151 if fp is not None or ui.canwritewithoutlabels():
152 152 out = fp or ui
153 153 if stat:
154 154 chunks = [patch.diffstat(util.iterlines(chunks), width=width)]
155 155 for chunk in util.filechunkiter(util.chunkbuffer(chunks)):
156 156 out.write(chunk)
157 157 else:
158 158 if stat:
159 159 chunks = patch.diffstatui(util.iterlines(chunks), width=width)
160 160 else:
161 161 chunks = patch.difflabel(
162 162 lambda chunks, **kwargs: chunks, chunks, opts=diffopts
163 163 )
164 164 if ui.canbatchlabeledwrites():
165 165
166 166 def gen():
167 167 for chunk, label in chunks:
168 168 yield ui.label(chunk, label=label)
169 169
170 170 for chunk in util.filechunkiter(util.chunkbuffer(gen())):
171 171 ui.write(chunk)
172 172 else:
173 173 for chunk, label in chunks:
174 174 ui.write(chunk, label=label)
175 175
176 176 for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
177 177 tempnode2 = node2
178 178 try:
179 179 if node2 is not None:
180 180 tempnode2 = ctx2.substate[subpath][1]
181 181 except KeyError:
182 182 # A subrepo that existed in node1 was deleted between node1 and
183 183 # node2 (inclusive). Thus, ctx2's substate won't contain that
184 184 # subpath. The best we can do is to ignore it.
185 185 tempnode2 = None
186 186 submatch = matchmod.subdirmatcher(subpath, match)
187 187 subprefix = repo.wvfs.reljoin(prefix, subpath)
188 188 if listsubrepos or match.exact(subpath) or any(submatch.files()):
189 189 sub.diff(
190 190 ui,
191 191 diffopts,
192 192 tempnode2,
193 193 submatch,
194 194 changes=changes,
195 195 stat=stat,
196 196 fp=fp,
197 197 prefix=subprefix,
198 198 )
199 199
200 200
201 201 class changesetdiffer(object):
202 202 """Generate diff of changeset with pre-configured filtering functions"""
203 203
204 204 def _makefilematcher(self, ctx):
205 205 return scmutil.matchall(ctx.repo())
206 206
207 207 def _makehunksfilter(self, ctx):
208 208 return None
209 209
210 210 def showdiff(self, ui, ctx, diffopts, graphwidth=0, stat=False):
211 211 repo = ctx.repo()
212 212 node = ctx.node()
213 213 prev = ctx.p1().node()
214 214 diffordiffstat(
215 215 ui,
216 216 repo,
217 217 diffopts,
218 218 prev,
219 219 node,
220 220 match=self._makefilematcher(ctx),
221 221 stat=stat,
222 222 graphwidth=graphwidth,
223 223 hunksfilterfn=self._makehunksfilter(ctx),
224 224 )
225 225
226 226
227 227 def changesetlabels(ctx):
228 228 labels = [b'log.changeset', b'changeset.%s' % ctx.phasestr()]
229 229 if ctx.obsolete():
230 230 labels.append(b'changeset.obsolete')
231 231 if ctx.isunstable():
232 232 labels.append(b'changeset.unstable')
233 233 for instability in ctx.instabilities():
234 234 labels.append(b'instability.%s' % instability)
235 235 return b' '.join(labels)
236 236
237 237
238 238 class changesetprinter(object):
239 239 '''show changeset information when templating not requested.'''
240 240
241 241 def __init__(self, ui, repo, differ=None, diffopts=None, buffered=False):
242 242 self.ui = ui
243 243 self.repo = repo
244 244 self.buffered = buffered
245 245 self._differ = differ or changesetdiffer()
246 246 self._diffopts = patch.diffallopts(ui, diffopts)
247 247 self._includestat = diffopts and diffopts.get(b'stat')
248 248 self._includediff = diffopts and diffopts.get(b'patch')
249 249 self.header = {}
250 250 self.hunk = {}
251 251 self.lastheader = None
252 252 self.footer = None
253 253 self._columns = templatekw.getlogcolumns()
254 254
255 255 def flush(self, ctx):
256 256 rev = ctx.rev()
257 257 if rev in self.header:
258 258 h = self.header[rev]
259 259 if h != self.lastheader:
260 260 self.lastheader = h
261 261 self.ui.write(h)
262 262 del self.header[rev]
263 263 if rev in self.hunk:
264 264 self.ui.write(self.hunk[rev])
265 265 del self.hunk[rev]
266 266
267 267 def close(self):
268 268 if self.footer:
269 269 self.ui.write(self.footer)
270 270
271 271 def show(self, ctx, copies=None, **props):
272 272 props = pycompat.byteskwargs(props)
273 273 if self.buffered:
274 274 self.ui.pushbuffer(labeled=True)
275 275 self._show(ctx, copies, props)
276 276 self.hunk[ctx.rev()] = self.ui.popbuffer()
277 277 else:
278 278 self._show(ctx, copies, props)
279 279
280 280 def _show(self, ctx, copies, props):
281 281 '''show a single changeset or file revision'''
282 282 changenode = ctx.node()
283 283 graphwidth = props.get(b'graphwidth', 0)
284 284
285 285 if self.ui.quiet:
286 286 self.ui.write(
287 287 b"%s\n" % scmutil.formatchangeid(ctx), label=b'log.node'
288 288 )
289 289 return
290 290
291 291 columns = self._columns
292 292 self.ui.write(
293 293 columns[b'changeset'] % scmutil.formatchangeid(ctx),
294 294 label=changesetlabels(ctx),
295 295 )
296 296
297 297 # branches are shown first before any other names due to backwards
298 298 # compatibility
299 299 branch = ctx.branch()
300 300 # don't show the default branch name
301 301 if branch != b'default':
302 302 self.ui.write(columns[b'branch'] % branch, label=b'log.branch')
303 303
304 304 for nsname, ns in pycompat.iteritems(self.repo.names):
305 305 # branches has special logic already handled above, so here we just
306 306 # skip it
307 307 if nsname == b'branches':
308 308 continue
309 309 # we will use the templatename as the color name since those two
310 310 # should be the same
311 311 for name in ns.names(self.repo, changenode):
312 312 self.ui.write(ns.logfmt % name, label=b'log.%s' % ns.colorname)
313 313 if self.ui.debugflag:
314 314 self.ui.write(
315 315 columns[b'phase'] % ctx.phasestr(), label=b'log.phase'
316 316 )
317 317 for pctx in scmutil.meaningfulparents(self.repo, ctx):
318 318 label = b'log.parent changeset.%s' % pctx.phasestr()
319 319 self.ui.write(
320 320 columns[b'parent'] % scmutil.formatchangeid(pctx), label=label
321 321 )
322 322
323 323 if self.ui.debugflag:
324 324 mnode = ctx.manifestnode()
325 325 if mnode is None:
326 326 mnode = wdirid
327 327 mrev = wdirrev
328 328 else:
329 329 mrev = self.repo.manifestlog.rev(mnode)
330 330 self.ui.write(
331 331 columns[b'manifest']
332 332 % scmutil.formatrevnode(self.ui, mrev, mnode),
333 333 label=b'ui.debug log.manifest',
334 334 )
335 335 self.ui.write(columns[b'user'] % ctx.user(), label=b'log.user')
336 336 self.ui.write(
337 337 columns[b'date'] % dateutil.datestr(ctx.date()), label=b'log.date'
338 338 )
339 339
340 340 if ctx.isunstable():
341 341 instabilities = ctx.instabilities()
342 342 self.ui.write(
343 343 columns[b'instability'] % b', '.join(instabilities),
344 344 label=b'log.instability',
345 345 )
346 346
347 347 elif ctx.obsolete():
348 348 self._showobsfate(ctx)
349 349
350 350 self._exthook(ctx)
351 351
352 352 if self.ui.debugflag:
353 353 files = ctx.p1().status(ctx)
354 354 for key, value in zip(
355 355 [b'files', b'files+', b'files-'],
356 356 [files.modified, files.added, files.removed],
357 357 ):
358 358 if value:
359 359 self.ui.write(
360 360 columns[key] % b" ".join(value),
361 361 label=b'ui.debug log.files',
362 362 )
363 363 elif ctx.files() and self.ui.verbose:
364 364 self.ui.write(
365 365 columns[b'files'] % b" ".join(ctx.files()),
366 366 label=b'ui.note log.files',
367 367 )
368 368 if copies and self.ui.verbose:
369 369 copies = [b'%s (%s)' % c for c in copies]
370 370 self.ui.write(
371 371 columns[b'copies'] % b' '.join(copies),
372 372 label=b'ui.note log.copies',
373 373 )
374 374
375 375 extra = ctx.extra()
376 376 if extra and self.ui.debugflag:
377 377 for key, value in sorted(extra.items()):
378 378 self.ui.write(
379 379 columns[b'extra'] % (key, stringutil.escapestr(value)),
380 380 label=b'ui.debug log.extra',
381 381 )
382 382
383 383 description = ctx.description().strip()
384 384 if description:
385 385 if self.ui.verbose:
386 386 self.ui.write(
387 387 _(b"description:\n"), label=b'ui.note log.description'
388 388 )
389 389 self.ui.write(description, label=b'ui.note log.description')
390 390 self.ui.write(b"\n\n")
391 391 else:
392 392 self.ui.write(
393 393 columns[b'summary'] % description.splitlines()[0],
394 394 label=b'log.summary',
395 395 )
396 396 self.ui.write(b"\n")
397 397
398 398 self._showpatch(ctx, graphwidth)
399 399
400 400 def _showobsfate(self, ctx):
401 401 # TODO: do not depend on templater
402 402 tres = formatter.templateresources(self.repo.ui, self.repo)
403 403 t = formatter.maketemplater(
404 404 self.repo.ui,
405 405 b'{join(obsfate, "\n")}',
406 406 defaults=templatekw.keywords,
407 407 resources=tres,
408 408 )
409 409 obsfate = t.renderdefault({b'ctx': ctx}).splitlines()
410 410
411 411 if obsfate:
412 412 for obsfateline in obsfate:
413 413 self.ui.write(
414 414 self._columns[b'obsolete'] % obsfateline,
415 415 label=b'log.obsfate',
416 416 )
417 417
418 418 def _exthook(self, ctx):
419 419 '''empty method used by extension as a hook point
420 420 '''
421 421
422 422 def _showpatch(self, ctx, graphwidth=0):
423 423 if self._includestat:
424 424 self._differ.showdiff(
425 425 self.ui, ctx, self._diffopts, graphwidth, stat=True
426 426 )
427 427 if self._includestat and self._includediff:
428 428 self.ui.write(b"\n")
429 429 if self._includediff:
430 430 self._differ.showdiff(
431 431 self.ui, ctx, self._diffopts, graphwidth, stat=False
432 432 )
433 433 if self._includestat or self._includediff:
434 434 self.ui.write(b"\n")
435 435
436 436
437 437 class changesetformatter(changesetprinter):
438 438 """Format changeset information by generic formatter"""
439 439
440 440 def __init__(
441 441 self, ui, repo, fm, differ=None, diffopts=None, buffered=False
442 442 ):
443 443 changesetprinter.__init__(self, ui, repo, differ, diffopts, buffered)
444 444 self._diffopts = patch.difffeatureopts(ui, diffopts, git=True)
445 445 self._fm = fm
446 446
447 447 def close(self):
448 448 self._fm.end()
449 449
450 450 def _show(self, ctx, copies, props):
451 451 '''show a single changeset or file revision'''
452 452 fm = self._fm
453 453 fm.startitem()
454 454 fm.context(ctx=ctx)
455 455 fm.data(rev=scmutil.intrev(ctx), node=fm.hexfunc(scmutil.binnode(ctx)))
456 456
457 457 datahint = fm.datahint()
458 458 if self.ui.quiet and not datahint:
459 459 return
460 460
461 461 fm.data(
462 462 branch=ctx.branch(),
463 463 phase=ctx.phasestr(),
464 464 user=ctx.user(),
465 465 date=fm.formatdate(ctx.date()),
466 466 desc=ctx.description(),
467 467 bookmarks=fm.formatlist(ctx.bookmarks(), name=b'bookmark'),
468 468 tags=fm.formatlist(ctx.tags(), name=b'tag'),
469 469 parents=fm.formatlist(
470 470 [fm.hexfunc(c.node()) for c in ctx.parents()], name=b'node'
471 471 ),
472 472 )
473 473
474 474 if self.ui.debugflag or b'manifest' in datahint:
475 475 fm.data(manifest=fm.hexfunc(ctx.manifestnode() or wdirid))
476 476 if self.ui.debugflag or b'extra' in datahint:
477 477 fm.data(extra=fm.formatdict(ctx.extra()))
478 478
479 479 if (
480 480 self.ui.debugflag
481 481 or b'modified' in datahint
482 482 or b'added' in datahint
483 483 or b'removed' in datahint
484 484 ):
485 485 files = ctx.p1().status(ctx)
486 486 fm.data(
487 487 modified=fm.formatlist(files.modified, name=b'file'),
488 488 added=fm.formatlist(files.added, name=b'file'),
489 489 removed=fm.formatlist(files.removed, name=b'file'),
490 490 )
491 491
492 492 verbose = not self.ui.debugflag and self.ui.verbose
493 493 if verbose or b'files' in datahint:
494 494 fm.data(files=fm.formatlist(ctx.files(), name=b'file'))
495 495 if verbose and copies or b'copies' in datahint:
496 496 fm.data(
497 497 copies=fm.formatdict(copies or {}, key=b'name', value=b'source')
498 498 )
499 499
500 500 if self._includestat or b'diffstat' in datahint:
501 501 self.ui.pushbuffer()
502 502 self._differ.showdiff(self.ui, ctx, self._diffopts, stat=True)
503 503 fm.data(diffstat=self.ui.popbuffer())
504 504 if self._includediff or b'diff' in datahint:
505 505 self.ui.pushbuffer()
506 506 self._differ.showdiff(self.ui, ctx, self._diffopts, stat=False)
507 507 fm.data(diff=self.ui.popbuffer())
508 508
509 509
510 510 class changesettemplater(changesetprinter):
511 511 '''format changeset information.
512 512
513 513 Note: there are a variety of convenience functions to build a
514 514 changesettemplater for common cases. See functions such as:
515 515 maketemplater, changesetdisplayer, buildcommittemplate, or other
516 516 functions that use changesest_templater.
517 517 '''
518 518
519 519 # Arguments before "buffered" used to be positional. Consider not
520 520 # adding/removing arguments before "buffered" to not break callers.
521 521 def __init__(
522 522 self, ui, repo, tmplspec, differ=None, diffopts=None, buffered=False
523 523 ):
524 524 changesetprinter.__init__(self, ui, repo, differ, diffopts, buffered)
525 525 # tres is shared with _graphnodeformatter()
526 526 self._tresources = tres = formatter.templateresources(ui, repo)
527 527 self.t = formatter.loadtemplater(
528 528 ui,
529 529 tmplspec,
530 530 defaults=templatekw.keywords,
531 531 resources=tres,
532 532 cache=templatekw.defaulttempl,
533 533 )
534 534 self._counter = itertools.count()
535 535
536 536 self._tref = tmplspec.ref
537 537 self._parts = {
538 538 b'header': b'',
539 539 b'footer': b'',
540 540 tmplspec.ref: tmplspec.ref,
541 541 b'docheader': b'',
542 542 b'docfooter': b'',
543 543 b'separator': b'',
544 544 }
545 545 if tmplspec.mapfile:
546 546 # find correct templates for current mode, for backward
547 547 # compatibility with 'log -v/-q/--debug' using a mapfile
548 548 tmplmodes = [
549 549 (True, b''),
550 550 (self.ui.verbose, b'_verbose'),
551 551 (self.ui.quiet, b'_quiet'),
552 552 (self.ui.debugflag, b'_debug'),
553 553 ]
554 554 for mode, postfix in tmplmodes:
555 555 for t in self._parts:
556 556 cur = t + postfix
557 557 if mode and cur in self.t:
558 558 self._parts[t] = cur
559 559 else:
560 560 partnames = [p for p in self._parts.keys() if p != tmplspec.ref]
561 561 m = formatter.templatepartsmap(tmplspec, self.t, partnames)
562 562 self._parts.update(m)
563 563
564 564 if self._parts[b'docheader']:
565 565 self.ui.write(self.t.render(self._parts[b'docheader'], {}))
566 566
567 567 def close(self):
568 568 if self._parts[b'docfooter']:
569 569 if not self.footer:
570 570 self.footer = b""
571 571 self.footer += self.t.render(self._parts[b'docfooter'], {})
572 572 return super(changesettemplater, self).close()
573 573
574 574 def _show(self, ctx, copies, props):
575 575 '''show a single changeset or file revision'''
576 576 props = props.copy()
577 577 props[b'ctx'] = ctx
578 578 props[b'index'] = index = next(self._counter)
579 579 props[b'revcache'] = {b'copies': copies}
580 580 graphwidth = props.get(b'graphwidth', 0)
581 581
582 582 # write separator, which wouldn't work well with the header part below
583 583 # since there's inherently a conflict between header (across items) and
584 584 # separator (per item)
585 585 if self._parts[b'separator'] and index > 0:
586 586 self.ui.write(self.t.render(self._parts[b'separator'], {}))
587 587
588 588 # write header
589 589 if self._parts[b'header']:
590 590 h = self.t.render(self._parts[b'header'], props)
591 591 if self.buffered:
592 592 self.header[ctx.rev()] = h
593 593 else:
594 594 if self.lastheader != h:
595 595 self.lastheader = h
596 596 self.ui.write(h)
597 597
598 598 # write changeset metadata, then patch if requested
599 599 key = self._parts[self._tref]
600 600 self.ui.write(self.t.render(key, props))
601 601 self._showpatch(ctx, graphwidth)
602 602
603 603 if self._parts[b'footer']:
604 604 if not self.footer:
605 605 self.footer = self.t.render(self._parts[b'footer'], props)
606 606
607 607
608 608 def templatespec(tmpl, mapfile):
609 609 if pycompat.ispy3:
610 610 assert not isinstance(tmpl, str), b'tmpl must not be a str'
611 611 if mapfile:
612 612 return formatter.templatespec(b'changeset', tmpl, mapfile)
613 613 else:
614 614 return formatter.templatespec(b'', tmpl, None)
615 615
616 616
617 617 def _lookuptemplate(ui, tmpl, style):
618 618 """Find the template matching the given template spec or style
619 619
620 620 See formatter.lookuptemplate() for details.
621 621 """
622 622
623 623 # ui settings
624 624 if not tmpl and not style: # template are stronger than style
625 625 tmpl = ui.config(b'ui', b'logtemplate')
626 626 if tmpl:
627 627 return templatespec(templater.unquotestring(tmpl), None)
628 628 else:
629 629 style = util.expandpath(ui.config(b'ui', b'style'))
630 630
631 631 if not tmpl and style:
632 632 mapfile = style
633 633 if not os.path.split(mapfile)[0]:
634 634 mapname = templater.templatepath(
635 635 b'map-cmdline.' + mapfile
636 636 ) or templater.templatepath(mapfile)
637 637 if mapname:
638 638 mapfile = mapname
639 639 return templatespec(None, mapfile)
640 640
641 641 return formatter.lookuptemplate(ui, b'changeset', tmpl)
642 642
643 643
644 644 def maketemplater(ui, repo, tmpl, buffered=False):
645 645 """Create a changesettemplater from a literal template 'tmpl'
646 646 byte-string."""
647 647 spec = templatespec(tmpl, None)
648 648 return changesettemplater(ui, repo, spec, buffered=buffered)
649 649
650 650
651 651 def changesetdisplayer(ui, repo, opts, differ=None, buffered=False):
652 652 """show one changeset using template or regular display.
653 653
654 654 Display format will be the first non-empty hit of:
655 655 1. option 'template'
656 656 2. option 'style'
657 657 3. [ui] setting 'logtemplate'
658 658 4. [ui] setting 'style'
659 659 If all of these values are either the unset or the empty string,
660 660 regular display via changesetprinter() is done.
661 661 """
662 662 postargs = (differ, opts, buffered)
663 663 spec = _lookuptemplate(ui, opts.get(b'template'), opts.get(b'style'))
664 664
665 665 # machine-readable formats have slightly different keyword set than
666 666 # plain templates, which are handled by changesetformatter.
667 667 # note that {b'pickle', b'debug'} can also be added to the list if needed.
668 668 if spec.ref in {b'cbor', b'json'}:
669 669 fm = ui.formatter(b'log', opts)
670 670 return changesetformatter(ui, repo, fm, *postargs)
671 671
672 672 if not spec.ref and not spec.tmpl and not spec.mapfile:
673 673 return changesetprinter(ui, repo, *postargs)
674 674
675 675 return changesettemplater(ui, repo, spec, *postargs)
676 676
677 677
678 678 def _makematcher(repo, revs, pats, opts):
679 679 """Build matcher and expanded patterns from log options
680 680
681 681 If --follow, revs are the revisions to follow from.
682 682
683 683 Returns (match, pats, slowpath) where
684 684 - match: a matcher built from the given pats and -I/-X opts
685 685 - pats: patterns used (globs are expanded on Windows)
686 686 - slowpath: True if patterns aren't as simple as scanning filelogs
687 687 """
688 688 # pats/include/exclude are passed to match.match() directly in
689 689 # _matchfiles() revset but walkchangerevs() builds its matcher with
690 690 # scmutil.match(). The difference is input pats are globbed on
691 691 # platforms without shell expansion (windows).
692 692 wctx = repo[None]
693 693 match, pats = scmutil.matchandpats(wctx, pats, opts)
694 694 slowpath = match.anypats() or (not match.always() and opts.get(b'removed'))
695 695 if not slowpath:
696 696 follow = opts.get(b'follow') or opts.get(b'follow_first')
697 697 startctxs = []
698 698 if follow and opts.get(b'rev'):
699 699 startctxs = [repo[r] for r in revs]
700 700 for f in match.files():
701 701 if follow and startctxs:
702 702 # No idea if the path was a directory at that revision, so
703 703 # take the slow path.
704 704 if any(f not in c for c in startctxs):
705 705 slowpath = True
706 706 continue
707 707 elif follow and f not in wctx:
708 708 # If the file exists, it may be a directory, so let it
709 709 # take the slow path.
710 710 if os.path.exists(repo.wjoin(f)):
711 711 slowpath = True
712 712 continue
713 713 else:
714 714 raise error.Abort(
715 715 _(
716 716 b'cannot follow file not in parent '
717 717 b'revision: "%s"'
718 718 )
719 719 % f
720 720 )
721 721 filelog = repo.file(f)
722 722 if not filelog:
723 723 # A zero count may be a directory or deleted file, so
724 724 # try to find matching entries on the slow path.
725 725 if follow:
726 726 raise error.Abort(
727 727 _(b'cannot follow nonexistent file: "%s"') % f
728 728 )
729 729 slowpath = True
730 730
731 731 # We decided to fall back to the slowpath because at least one
732 732 # of the paths was not a file. Check to see if at least one of them
733 733 # existed in history - in that case, we'll continue down the
734 734 # slowpath; otherwise, we can turn off the slowpath
735 735 if slowpath:
736 736 for path in match.files():
737 737 if path == b'.' or path in repo.store:
738 738 break
739 739 else:
740 740 slowpath = False
741 741
742 742 return match, pats, slowpath
743 743
744 744
745 745 def _fileancestors(repo, revs, match, followfirst):
746 746 fctxs = []
747 747 for r in revs:
748 748 ctx = repo[r]
749 749 fctxs.extend(ctx[f].introfilectx() for f in ctx.walk(match))
750 750
751 751 # When displaying a revision with --patch --follow FILE, we have
752 752 # to know which file of the revision must be diffed. With
753 753 # --follow, we want the names of the ancestors of FILE in the
754 754 # revision, stored in "fcache". "fcache" is populated as a side effect
755 755 # of the graph traversal.
756 756 fcache = {}
757 757
758 758 def filematcher(ctx):
759 759 return scmutil.matchfiles(repo, fcache.get(ctx.rev(), []))
760 760
761 761 def revgen():
762 762 for rev, cs in dagop.filectxancestors(fctxs, followfirst=followfirst):
763 763 fcache[rev] = [c.path() for c in cs]
764 764 yield rev
765 765
766 766 return smartset.generatorset(revgen(), iterasc=False), filematcher
767 767
768 768
769 769 def _makenofollowfilematcher(repo, pats, opts):
770 770 '''hook for extensions to override the filematcher for non-follow cases'''
771 771 return None
772 772
773 773
774 774 _opt2logrevset = {
775 775 b'no_merges': (b'not merge()', None),
776 776 b'only_merges': (b'merge()', None),
777 777 b'_matchfiles': (None, b'_matchfiles(%ps)'),
778 778 b'date': (b'date(%s)', None),
779 779 b'branch': (b'branch(%s)', b'%lr'),
780 780 b'_patslog': (b'filelog(%s)', b'%lr'),
781 781 b'keyword': (b'keyword(%s)', b'%lr'),
782 782 b'prune': (b'ancestors(%s)', b'not %lr'),
783 783 b'user': (b'user(%s)', b'%lr'),
784 784 }
785 785
786 786
787 787 def _makerevset(repo, match, pats, slowpath, opts):
788 788 """Return a revset string built from log options and file patterns"""
789 789 opts = dict(opts)
790 790 # follow or not follow?
791 791 follow = opts.get(b'follow') or opts.get(b'follow_first')
792 792
793 793 # branch and only_branch are really aliases and must be handled at
794 794 # the same time
795 795 opts[b'branch'] = opts.get(b'branch', []) + opts.get(b'only_branch', [])
796 796 opts[b'branch'] = [repo.lookupbranch(b) for b in opts[b'branch']]
797 797
798 798 if slowpath:
799 799 # See walkchangerevs() slow path.
800 800 #
801 801 # pats/include/exclude cannot be represented as separate
802 802 # revset expressions as their filtering logic applies at file
803 803 # level. For instance "-I a -X b" matches a revision touching
804 804 # "a" and "b" while "file(a) and not file(b)" does
805 805 # not. Besides, filesets are evaluated against the working
806 806 # directory.
807 807 matchargs = [b'r:', b'd:relpath']
808 808 for p in pats:
809 809 matchargs.append(b'p:' + p)
810 810 for p in opts.get(b'include', []):
811 811 matchargs.append(b'i:' + p)
812 812 for p in opts.get(b'exclude', []):
813 813 matchargs.append(b'x:' + p)
814 814 opts[b'_matchfiles'] = matchargs
815 815 elif not follow:
816 816 opts[b'_patslog'] = list(pats)
817 817
818 818 expr = []
819 819 for op, val in sorted(pycompat.iteritems(opts)):
820 820 if not val:
821 821 continue
822 822 if op not in _opt2logrevset:
823 823 continue
824 824 revop, listop = _opt2logrevset[op]
825 825 if revop and b'%' not in revop:
826 826 expr.append(revop)
827 827 elif not listop:
828 828 expr.append(revsetlang.formatspec(revop, val))
829 829 else:
830 830 if revop:
831 831 val = [revsetlang.formatspec(revop, v) for v in val]
832 832 expr.append(revsetlang.formatspec(listop, val))
833 833
834 834 if expr:
835 835 expr = b'(' + b' and '.join(expr) + b')'
836 836 else:
837 837 expr = None
838 838 return expr
839 839
840 840
841 841 def _initialrevs(repo, opts):
842 842 """Return the initial set of revisions to be filtered or followed"""
843 843 follow = opts.get(b'follow') or opts.get(b'follow_first')
844 844 if opts.get(b'rev'):
845 845 revs = scmutil.revrange(repo, opts[b'rev'])
846 846 elif follow and repo.dirstate.p1() == nullid:
847 847 revs = smartset.baseset()
848 848 elif follow:
849 849 revs = repo.revs(b'.')
850 850 else:
851 851 revs = smartset.spanset(repo)
852 852 revs.reverse()
853 853 return revs
854 854
855 855
856 856 def getrevs(repo, pats, opts):
857 857 # type: (Any, Any, Any) -> Tuple[smartset.abstractsmartset, Optional[changesetdiffer]]
858 858 """Return (revs, differ) where revs is a smartset
859 859
860 860 differ is a changesetdiffer with pre-configured file matcher.
861 861 """
862 862 follow = opts.get(b'follow') or opts.get(b'follow_first')
863 863 followfirst = opts.get(b'follow_first')
864 864 limit = getlimit(opts)
865 865 revs = _initialrevs(repo, opts)
866 866 if not revs:
867 867 return smartset.baseset(), None
868 868 match, pats, slowpath = _makematcher(repo, revs, pats, opts)
869 869 filematcher = None
870 870 if follow:
871 871 if slowpath or match.always():
872 872 revs = dagop.revancestors(repo, revs, followfirst=followfirst)
873 873 else:
874 874 revs, filematcher = _fileancestors(repo, revs, match, followfirst)
875 875 revs.reverse()
876 876 if filematcher is None:
877 877 filematcher = _makenofollowfilematcher(repo, pats, opts)
878 878 if filematcher is None:
879 879
880 880 def filematcher(ctx):
881 881 return match
882 882
883 883 expr = _makerevset(repo, match, pats, slowpath, opts)
884 884 if opts.get(b'graph'):
885 885 # User-specified revs might be unsorted, but don't sort before
886 886 # _makerevset because it might depend on the order of revs
887 887 if repo.ui.configbool(b'experimental', b'log.topo'):
888 888 if not revs.istopo():
889 889 revs = dagop.toposort(revs, repo.changelog.parentrevs)
890 890 # TODO: try to iterate the set lazily
891 891 revs = revset.baseset(list(revs), istopo=True)
892 892 elif not (revs.isdescending() or revs.istopo()):
893 893 revs.sort(reverse=True)
894 894 if expr:
895 895 matcher = revset.match(None, expr)
896 896 revs = matcher(repo, revs)
897 897 if limit is not None:
898 898 revs = revs.slice(0, limit)
899 899
900 900 differ = changesetdiffer()
901 901 differ._makefilematcher = filematcher
902 902 return revs, differ
903 903
904 904
905 905 def _parselinerangeopt(repo, opts):
906 906 """Parse --line-range log option and return a list of tuples (filename,
907 907 (fromline, toline)).
908 908 """
909 909 linerangebyfname = []
910 910 for pat in opts.get(b'line_range', []):
911 911 try:
912 912 pat, linerange = pat.rsplit(b',', 1)
913 913 except ValueError:
914 914 raise error.Abort(_(b'malformatted line-range pattern %s') % pat)
915 915 try:
916 916 fromline, toline = map(int, linerange.split(b':'))
917 917 except ValueError:
918 918 raise error.Abort(_(b"invalid line range for %s") % pat)
919 919 msg = _(b"line range pattern '%s' must match exactly one file") % pat
920 920 fname = scmutil.parsefollowlinespattern(repo, None, pat, msg)
921 921 linerangebyfname.append(
922 922 (fname, util.processlinerange(fromline, toline))
923 923 )
924 924 return linerangebyfname
925 925
926 926
927 927 def getlinerangerevs(repo, userrevs, opts):
928 928 """Return (revs, differ).
929 929
930 930 "revs" are revisions obtained by processing "line-range" log options and
931 931 walking block ancestors of each specified file/line-range.
932 932
933 933 "differ" is a changesetdiffer with pre-configured file matcher and hunks
934 934 filter.
935 935 """
936 936 wctx = repo[None]
937 937
938 938 # Two-levels map of "rev -> file ctx -> [line range]".
939 939 linerangesbyrev = {}
940 940 for fname, (fromline, toline) in _parselinerangeopt(repo, opts):
941 941 if fname not in wctx:
942 942 raise error.Abort(
943 943 _(b'cannot follow file not in parent revision: "%s"') % fname
944 944 )
945 945 fctx = wctx.filectx(fname)
946 946 for fctx, linerange in dagop.blockancestors(fctx, fromline, toline):
947 947 rev = fctx.introrev()
948 if rev is None:
949 rev = wdirrev
948 950 if rev not in userrevs:
949 951 continue
950 952 linerangesbyrev.setdefault(rev, {}).setdefault(
951 953 fctx.path(), []
952 954 ).append(linerange)
953 955
954 956 def nofilterhunksfn(fctx, hunks):
955 957 return hunks
956 958
957 959 def hunksfilter(ctx):
958 fctxlineranges = linerangesbyrev.get(ctx.rev())
960 fctxlineranges = linerangesbyrev.get(scmutil.intrev(ctx))
959 961 if fctxlineranges is None:
960 962 return nofilterhunksfn
961 963
962 964 def filterfn(fctx, hunks):
963 965 lineranges = fctxlineranges.get(fctx.path())
964 966 if lineranges is not None:
965 967 for hr, lines in hunks:
966 968 if hr is None: # binary
967 969 yield hr, lines
968 970 continue
969 971 if any(mdiff.hunkinrange(hr[2:], lr) for lr in lineranges):
970 972 yield hr, lines
971 973 else:
972 974 for hunk in hunks:
973 975 yield hunk
974 976
975 977 return filterfn
976 978
977 979 def filematcher(ctx):
978 files = list(linerangesbyrev.get(ctx.rev(), []))
980 files = list(linerangesbyrev.get(scmutil.intrev(ctx), []))
979 981 return scmutil.matchfiles(repo, files)
980 982
981 983 revs = sorted(linerangesbyrev, reverse=True)
982 984
983 985 differ = changesetdiffer()
984 986 differ._makefilematcher = filematcher
985 987 differ._makehunksfilter = hunksfilter
986 988 return smartset.baseset(revs), differ
987 989
988 990
989 991 def _graphnodeformatter(ui, displayer):
990 992 spec = ui.config(b'ui', b'graphnodetemplate')
991 993 if not spec:
992 994 return templatekw.getgraphnode # fast path for "{graphnode}"
993 995
994 996 spec = templater.unquotestring(spec)
995 997 if isinstance(displayer, changesettemplater):
996 998 # reuse cache of slow templates
997 999 tres = displayer._tresources
998 1000 else:
999 1001 tres = formatter.templateresources(ui)
1000 1002 templ = formatter.maketemplater(
1001 1003 ui, spec, defaults=templatekw.keywords, resources=tres
1002 1004 )
1003 1005
1004 1006 def formatnode(repo, ctx):
1005 1007 props = {b'ctx': ctx, b'repo': repo}
1006 1008 return templ.renderdefault(props)
1007 1009
1008 1010 return formatnode
1009 1011
1010 1012
1011 1013 def displaygraph(ui, repo, dag, displayer, edgefn, getcopies=None, props=None):
1012 1014 props = props or {}
1013 1015 formatnode = _graphnodeformatter(ui, displayer)
1014 1016 state = graphmod.asciistate()
1015 1017 styles = state.styles
1016 1018
1017 1019 # only set graph styling if HGPLAIN is not set.
1018 1020 if ui.plain(b'graph'):
1019 1021 # set all edge styles to |, the default pre-3.8 behaviour
1020 1022 styles.update(dict.fromkeys(styles, b'|'))
1021 1023 else:
1022 1024 edgetypes = {
1023 1025 b'parent': graphmod.PARENT,
1024 1026 b'grandparent': graphmod.GRANDPARENT,
1025 1027 b'missing': graphmod.MISSINGPARENT,
1026 1028 }
1027 1029 for name, key in edgetypes.items():
1028 1030 # experimental config: experimental.graphstyle.*
1029 1031 styles[key] = ui.config(
1030 1032 b'experimental', b'graphstyle.%s' % name, styles[key]
1031 1033 )
1032 1034 if not styles[key]:
1033 1035 styles[key] = None
1034 1036
1035 1037 # experimental config: experimental.graphshorten
1036 1038 state.graphshorten = ui.configbool(b'experimental', b'graphshorten')
1037 1039
1038 1040 for rev, type, ctx, parents in dag:
1039 1041 char = formatnode(repo, ctx)
1040 1042 copies = getcopies(ctx) if getcopies else None
1041 1043 edges = edgefn(type, char, state, rev, parents)
1042 1044 firstedge = next(edges)
1043 1045 width = firstedge[2]
1044 1046 displayer.show(
1045 1047 ctx, copies=copies, graphwidth=width, **pycompat.strkwargs(props)
1046 1048 )
1047 1049 lines = displayer.hunk.pop(rev).split(b'\n')
1048 1050 if not lines[-1]:
1049 1051 del lines[-1]
1050 1052 displayer.flush(ctx)
1051 1053 for type, char, width, coldata in itertools.chain([firstedge], edges):
1052 1054 graphmod.ascii(ui, state, type, char, lines, coldata)
1053 1055 lines = []
1054 1056 displayer.close()
1055 1057
1056 1058
1057 1059 def displaygraphrevs(ui, repo, revs, displayer, getrenamed):
1058 1060 revdag = graphmod.dagwalker(repo, revs)
1059 1061 displaygraph(ui, repo, revdag, displayer, graphmod.asciiedges, getrenamed)
1060 1062
1061 1063
1062 1064 def displayrevs(ui, repo, revs, displayer, getcopies):
1063 1065 for rev in revs:
1064 1066 ctx = repo[rev]
1065 1067 copies = getcopies(ctx) if getcopies else None
1066 1068 displayer.show(ctx, copies=copies)
1067 1069 displayer.flush(ctx)
1068 1070 displayer.close()
1069 1071
1070 1072
1071 1073 def checkunsupportedgraphflags(pats, opts):
1072 1074 for op in [b"newest_first"]:
1073 1075 if op in opts and opts[op]:
1074 1076 raise error.Abort(
1075 1077 _(b"-G/--graph option is incompatible with --%s")
1076 1078 % op.replace(b"_", b"-")
1077 1079 )
1078 1080
1079 1081
1080 1082 def graphrevs(repo, nodes, opts):
1081 1083 limit = getlimit(opts)
1082 1084 nodes.reverse()
1083 1085 if limit is not None:
1084 1086 nodes = nodes[:limit]
1085 1087 return graphmod.nodes(repo, nodes)
@@ -1,228 +1,228 b''
1 1 # pvec.py - probabilistic vector clocks for Mercurial
2 2 #
3 3 # Copyright 2012 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 '''
9 9 A "pvec" is a changeset property based on the theory of vector clocks
10 10 that can be compared to discover relatedness without consulting a
11 11 graph. This can be useful for tasks like determining how a
12 12 disconnected patch relates to a repository.
13 13
14 14 Currently a pvec consist of 448 bits, of which 24 are 'depth' and the
15 15 remainder are a bit vector. It is represented as a 70-character base85
16 16 string.
17 17
18 18 Construction:
19 19
20 20 - a root changeset has a depth of 0 and a bit vector based on its hash
21 21 - a normal commit has a changeset where depth is increased by one and
22 22 one bit vector bit is flipped based on its hash
23 23 - a merge changeset pvec is constructed by copying changes from one pvec into
24 24 the other to balance its depth
25 25
26 26 Properties:
27 27
28 28 - for linear changes, difference in depth is always <= hamming distance
29 29 - otherwise, changes are probably divergent
30 30 - when hamming distance is < 200, we can reliably detect when pvecs are near
31 31
32 32 Issues:
33 33
34 34 - hamming distance ceases to work over distances of ~ 200
35 35 - detecting divergence is less accurate when the common ancestor is very close
36 36 to either revision or total distance is high
37 37 - this could probably be improved by modeling the relation between
38 38 delta and hdist
39 39
40 40 Uses:
41 41
42 42 - a patch pvec can be used to locate the nearest available common ancestor for
43 43 resolving conflicts
44 44 - ordering of patches can be established without a DAG
45 45 - two head pvecs can be compared to determine whether push/pull/merge is needed
46 46 and approximately how many changesets are involved
47 47 - can be used to find a heuristic divergence measure between changesets on
48 48 different branches
49 49 '''
50 50
51 51 from __future__ import absolute_import, division
52 52
53 53 from .node import nullrev
54 54 from . import (
55 55 pycompat,
56 56 util,
57 57 )
58 58
59 59 _size = 448 # 70 chars b85-encoded
60 60 _bytes = _size // 8
61 61 _depthbits = 24
62 62 _depthbytes = _depthbits // 8
63 63 _vecbytes = _bytes - _depthbytes
64 64 _vecbits = _vecbytes * 8
65 65 _radius = (_vecbits - 30) // 2 # high probability vectors are related
66 66
67 67
68 68 def _bin(bs):
69 69 '''convert a bytestring to a long'''
70 70 v = 0
71 71 for b in bs:
72 72 v = v * 256 + ord(b)
73 73 return v
74 74
75 75
76 76 def _str(v, l):
77 77 # type: (int, int) -> bytes
78 78 bs = b""
79 79 for p in pycompat.xrange(l):
80 80 bs = pycompat.bytechr(v & 255) + bs
81 81 v >>= 8
82 82 return bs
83 83
84 84
85 85 def _split(b):
86 86 '''depth and bitvec'''
87 87 return _bin(b[:_depthbytes]), _bin(b[_depthbytes:])
88 88
89 89
90 90 def _join(depth, bitvec):
91 91 return _str(depth, _depthbytes) + _str(bitvec, _vecbytes)
92 92
93 93
94 94 def _hweight(x):
95 95 c = 0
96 96 while x:
97 97 if x & 1:
98 98 c += 1
99 99 x >>= 1
100 100 return c
101 101
102 102
103 103 _htab = [_hweight(x) for x in pycompat.xrange(256)]
104 104
105 105
106 106 def _hamming(a, b):
107 107 '''find the hamming distance between two longs'''
108 108 d = a ^ b
109 109 c = 0
110 110 while d:
111 111 c += _htab[d & 0xFF]
112 112 d >>= 8
113 113 return c
114 114
115 115
116 116 def _mergevec(x, y, c):
117 117 # Ideally, this function would be x ^ y ^ ancestor, but finding
118 118 # ancestors is a nuisance. So instead we find the minimal number
119 119 # of changes to balance the depth and hamming distance
120 120
121 121 d1, v1 = x
122 122 d2, v2 = y
123 123 if d1 < d2:
124 124 d1, d2, v1, v2 = d2, d1, v2, v1
125 125
126 126 hdist = _hamming(v1, v2)
127 127 ddist = d1 - d2
128 128 v = v1
129 129 m = v1 ^ v2 # mask of different bits
130 130 i = 1
131 131
132 132 if hdist > ddist:
133 133 # if delta = 10 and hdist = 100, then we need to go up 55 steps
134 134 # to the ancestor and down 45
135 135 changes = (hdist - ddist + 1) // 2
136 136 else:
137 137 # must make at least one change
138 138 changes = 1
139 139 depth = d1 + changes
140 140
141 141 # copy changes from v2
142 142 if m:
143 143 while changes:
144 144 if m & i:
145 145 v ^= i
146 146 changes -= 1
147 147 i <<= 1
148 148 else:
149 149 v = _flipbit(v, c)
150 150
151 151 return depth, v
152 152
153 153
154 154 def _flipbit(v, node):
155 155 # converting bit strings to longs is slow
156 156 bit = (hash(node) & 0xFFFFFFFF) % _vecbits
157 157 return v ^ (1 << bit)
158 158
159 159
160 160 def ctxpvec(ctx):
161 161 '''construct a pvec for ctx while filling in the cache'''
162 162 r = ctx.repo()
163 163 if not util.safehasattr(r, "_pveccache"):
164 164 r._pveccache = {}
165 165 pvc = r._pveccache
166 166 if ctx.rev() not in pvc:
167 167 cl = r.changelog
168 168 for n in pycompat.xrange(ctx.rev() + 1):
169 169 if n not in pvc:
170 170 node = cl.node(n)
171 171 p1, p2 = cl.parentrevs(n)
172 172 if p1 == nullrev:
173 173 # start with a 'random' vector at root
174 174 pvc[n] = (0, _bin((node * 3)[:_vecbytes]))
175 175 elif p2 == nullrev:
176 176 d, v = pvc[p1]
177 177 pvc[n] = (d + 1, _flipbit(v, node))
178 178 else:
179 179 pvc[n] = _mergevec(pvc[p1], pvc[p2], node)
180 180 bs = _join(*pvc[ctx.rev()])
181 181 return pvec(util.b85encode(bs))
182 182
183 183
184 184 class pvec(object):
185 185 def __init__(self, hashorctx):
186 if isinstance(hashorctx, str):
186 if isinstance(hashorctx, bytes):
187 187 self._bs = hashorctx
188 188 self._depth, self._vec = _split(util.b85decode(hashorctx))
189 189 else:
190 190 self._vec = ctxpvec(hashorctx)
191 191
192 192 def __str__(self):
193 193 return self._bs
194 194
195 195 def __eq__(self, b):
196 196 return self._vec == b._vec and self._depth == b._depth
197 197
198 198 def __lt__(self, b):
199 199 delta = b._depth - self._depth
200 200 if delta < 0:
201 201 return False # always correct
202 202 if _hamming(self._vec, b._vec) > delta:
203 203 return False
204 204 return True
205 205
206 206 def __gt__(self, b):
207 207 return b < self
208 208
209 209 def __or__(self, b):
210 210 delta = abs(b._depth - self._depth)
211 211 if _hamming(self._vec, b._vec) <= delta:
212 212 return False
213 213 return True
214 214
215 215 def __sub__(self, b):
216 216 if self | b:
217 217 raise ValueError(b"concurrent pvecs")
218 218 return self._depth - b._depth
219 219
220 220 def distance(self, b):
221 221 d = abs(b._depth - self._depth)
222 222 h = _hamming(self._vec, b._vec)
223 223 return max(d, h)
224 224
225 225 def near(self, b):
226 226 dist = abs(b.depth - self._depth)
227 227 if dist > _radius or _hamming(self._vec, b._vec) > _radius:
228 228 return False
@@ -1,537 +1,537 b''
1 1 # repair.py - functions for repository repair for mercurial
2 2 #
3 3 # Copyright 2005, 2006 Chris Mason <mason@suse.com>
4 4 # Copyright 2007 Matt Mackall
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from __future__ import absolute_import
10 10
11 11 import errno
12 12 import hashlib
13 13
14 14 from .i18n import _
15 15 from .node import (
16 16 hex,
17 17 short,
18 18 )
19 19 from . import (
20 20 bundle2,
21 21 changegroup,
22 22 discovery,
23 23 error,
24 24 exchange,
25 25 obsolete,
26 26 obsutil,
27 27 pathutil,
28 28 phases,
29 29 pycompat,
30 30 util,
31 31 )
32 32 from .utils import stringutil
33 33
34 34
35 35 def backupbundle(
36 36 repo, bases, heads, node, suffix, compress=True, obsolescence=True
37 37 ):
38 38 """create a bundle with the specified revisions as a backup"""
39 39
40 40 backupdir = b"strip-backup"
41 41 vfs = repo.vfs
42 42 if not vfs.isdir(backupdir):
43 43 vfs.mkdir(backupdir)
44 44
45 45 # Include a hash of all the nodes in the filename for uniqueness
46 46 allcommits = repo.set(b'%ln::%ln', bases, heads)
47 47 allhashes = sorted(c.hex() for c in allcommits)
48 48 totalhash = hashlib.sha1(b''.join(allhashes)).digest()
49 49 name = b"%s/%s-%s-%s.hg" % (
50 50 backupdir,
51 51 short(node),
52 52 hex(totalhash[:4]),
53 53 suffix,
54 54 )
55 55
56 56 cgversion = changegroup.localversion(repo)
57 57 comp = None
58 58 if cgversion != b'01':
59 59 bundletype = b"HG20"
60 60 if compress:
61 61 comp = b'BZ'
62 62 elif compress:
63 63 bundletype = b"HG10BZ"
64 64 else:
65 65 bundletype = b"HG10UN"
66 66
67 67 outgoing = discovery.outgoing(repo, missingroots=bases, missingheads=heads)
68 68 contentopts = {
69 69 b'cg.version': cgversion,
70 70 b'obsolescence': obsolescence,
71 71 b'phases': True,
72 72 }
73 73 return bundle2.writenewbundle(
74 74 repo.ui,
75 75 repo,
76 76 b'strip',
77 77 name,
78 78 bundletype,
79 79 outgoing,
80 80 contentopts,
81 81 vfs,
82 82 compression=comp,
83 83 )
84 84
85 85
86 86 def _collectfiles(repo, striprev):
87 87 """find out the filelogs affected by the strip"""
88 88 files = set()
89 89
90 90 for x in pycompat.xrange(striprev, len(repo)):
91 91 files.update(repo[x].files())
92 92
93 93 return sorted(files)
94 94
95 95
96 96 def _collectrevlog(revlog, striprev):
97 97 _, brokenset = revlog.getstrippoint(striprev)
98 98 return [revlog.linkrev(r) for r in brokenset]
99 99
100 100
101 101 def _collectbrokencsets(repo, files, striprev):
102 102 """return the changesets which will be broken by the truncation"""
103 103 s = set()
104 104
105 105 for revlog in manifestrevlogs(repo):
106 106 s.update(_collectrevlog(revlog, striprev))
107 107 for fname in files:
108 108 s.update(_collectrevlog(repo.file(fname), striprev))
109 109
110 110 return s
111 111
112 112
113 113 def strip(ui, repo, nodelist, backup=True, topic=b'backup'):
114 114 # This function requires the caller to lock the repo, but it operates
115 115 # within a transaction of its own, and thus requires there to be no current
116 116 # transaction when it is called.
117 117 if repo.currenttransaction() is not None:
118 118 raise error.ProgrammingError(b'cannot strip from inside a transaction')
119 119
120 120 # Simple way to maintain backwards compatibility for this
121 121 # argument.
122 122 if backup in [b'none', b'strip']:
123 123 backup = False
124 124
125 125 repo = repo.unfiltered()
126 126 repo.destroying()
127 127 vfs = repo.vfs
128 128 # load bookmark before changelog to avoid side effect from outdated
129 129 # changelog (see repo._refreshchangelog)
130 130 repo._bookmarks
131 131 cl = repo.changelog
132 132
133 133 # TODO handle undo of merge sets
134 if isinstance(nodelist, str):
134 if isinstance(nodelist, bytes):
135 135 nodelist = [nodelist]
136 136 striplist = [cl.rev(node) for node in nodelist]
137 137 striprev = min(striplist)
138 138
139 139 files = _collectfiles(repo, striprev)
140 140 saverevs = _collectbrokencsets(repo, files, striprev)
141 141
142 142 # Some revisions with rev > striprev may not be descendants of striprev.
143 143 # We have to find these revisions and put them in a bundle, so that
144 144 # we can restore them after the truncations.
145 145 # To create the bundle we use repo.changegroupsubset which requires
146 146 # the list of heads and bases of the set of interesting revisions.
147 147 # (head = revision in the set that has no descendant in the set;
148 148 # base = revision in the set that has no ancestor in the set)
149 149 tostrip = set(striplist)
150 150 saveheads = set(saverevs)
151 151 for r in cl.revs(start=striprev + 1):
152 152 if any(p in tostrip for p in cl.parentrevs(r)):
153 153 tostrip.add(r)
154 154
155 155 if r not in tostrip:
156 156 saverevs.add(r)
157 157 saveheads.difference_update(cl.parentrevs(r))
158 158 saveheads.add(r)
159 159 saveheads = [cl.node(r) for r in saveheads]
160 160
161 161 # compute base nodes
162 162 if saverevs:
163 163 descendants = set(cl.descendants(saverevs))
164 164 saverevs.difference_update(descendants)
165 165 savebases = [cl.node(r) for r in saverevs]
166 166 stripbases = [cl.node(r) for r in tostrip]
167 167
168 168 stripobsidx = obsmarkers = ()
169 169 if repo.ui.configbool(b'devel', b'strip-obsmarkers'):
170 170 obsmarkers = obsutil.exclusivemarkers(repo, stripbases)
171 171 if obsmarkers:
172 172 stripobsidx = [
173 173 i for i, m in enumerate(repo.obsstore) if m in obsmarkers
174 174 ]
175 175
176 176 newbmtarget, updatebm = _bookmarkmovements(repo, tostrip)
177 177
178 178 backupfile = None
179 179 node = nodelist[-1]
180 180 if backup:
181 181 backupfile = _createstripbackup(repo, stripbases, node, topic)
182 182 # create a changegroup for all the branches we need to keep
183 183 tmpbundlefile = None
184 184 if saveheads:
185 185 # do not compress temporary bundle if we remove it from disk later
186 186 #
187 187 # We do not include obsolescence, it might re-introduce prune markers
188 188 # we are trying to strip. This is harmless since the stripped markers
189 189 # are already backed up and we did not touched the markers for the
190 190 # saved changesets.
191 191 tmpbundlefile = backupbundle(
192 192 repo,
193 193 savebases,
194 194 saveheads,
195 195 node,
196 196 b'temp',
197 197 compress=False,
198 198 obsolescence=False,
199 199 )
200 200
201 201 with ui.uninterruptible():
202 202 try:
203 203 with repo.transaction(b"strip") as tr:
204 204 # TODO this code violates the interface abstraction of the
205 205 # transaction and makes assumptions that file storage is
206 206 # using append-only files. We'll need some kind of storage
207 207 # API to handle stripping for us.
208 208 offset = len(tr._entries)
209 209
210 210 tr.startgroup()
211 211 cl.strip(striprev, tr)
212 212 stripmanifest(repo, striprev, tr, files)
213 213
214 214 for fn in files:
215 215 repo.file(fn).strip(striprev, tr)
216 216 tr.endgroup()
217 217
218 218 for i in pycompat.xrange(offset, len(tr._entries)):
219 219 file, troffset, ignore = tr._entries[i]
220 220 with repo.svfs(file, b'a', checkambig=True) as fp:
221 221 fp.truncate(troffset)
222 222 if troffset == 0:
223 223 repo.store.markremoved(file)
224 224
225 225 deleteobsmarkers(repo.obsstore, stripobsidx)
226 226 del repo.obsstore
227 227 repo.invalidatevolatilesets()
228 228 repo._phasecache.filterunknown(repo)
229 229
230 230 if tmpbundlefile:
231 231 ui.note(_(b"adding branch\n"))
232 232 f = vfs.open(tmpbundlefile, b"rb")
233 233 gen = exchange.readbundle(ui, f, tmpbundlefile, vfs)
234 234 if not repo.ui.verbose:
235 235 # silence internal shuffling chatter
236 236 repo.ui.pushbuffer()
237 237 tmpbundleurl = b'bundle:' + vfs.join(tmpbundlefile)
238 238 txnname = b'strip'
239 239 if not isinstance(gen, bundle2.unbundle20):
240 240 txnname = b"strip\n%s" % util.hidepassword(tmpbundleurl)
241 241 with repo.transaction(txnname) as tr:
242 242 bundle2.applybundle(
243 243 repo, gen, tr, source=b'strip', url=tmpbundleurl
244 244 )
245 245 if not repo.ui.verbose:
246 246 repo.ui.popbuffer()
247 247 f.close()
248 248
249 249 with repo.transaction(b'repair') as tr:
250 250 bmchanges = [(m, repo[newbmtarget].node()) for m in updatebm]
251 251 repo._bookmarks.applychanges(repo, tr, bmchanges)
252 252
253 253 # remove undo files
254 254 for undovfs, undofile in repo.undofiles():
255 255 try:
256 256 undovfs.unlink(undofile)
257 257 except OSError as e:
258 258 if e.errno != errno.ENOENT:
259 259 ui.warn(
260 260 _(b'error removing %s: %s\n')
261 261 % (
262 262 undovfs.join(undofile),
263 263 stringutil.forcebytestr(e),
264 264 )
265 265 )
266 266
267 267 except: # re-raises
268 268 if backupfile:
269 269 ui.warn(
270 270 _(b"strip failed, backup bundle stored in '%s'\n")
271 271 % vfs.join(backupfile)
272 272 )
273 273 if tmpbundlefile:
274 274 ui.warn(
275 275 _(b"strip failed, unrecovered changes stored in '%s'\n")
276 276 % vfs.join(tmpbundlefile)
277 277 )
278 278 ui.warn(
279 279 _(
280 280 b"(fix the problem, then recover the changesets with "
281 281 b"\"hg unbundle '%s'\")\n"
282 282 )
283 283 % vfs.join(tmpbundlefile)
284 284 )
285 285 raise
286 286 else:
287 287 if tmpbundlefile:
288 288 # Remove temporary bundle only if there were no exceptions
289 289 vfs.unlink(tmpbundlefile)
290 290
291 291 repo.destroyed()
292 292 # return the backup file path (or None if 'backup' was False) so
293 293 # extensions can use it
294 294 return backupfile
295 295
296 296
297 297 def softstrip(ui, repo, nodelist, backup=True, topic=b'backup'):
298 298 """perform a "soft" strip using the archived phase"""
299 299 tostrip = [c.node() for c in repo.set(b'sort(%ln::)', nodelist)]
300 300 if not tostrip:
301 301 return None
302 302
303 303 newbmtarget, updatebm = _bookmarkmovements(repo, tostrip)
304 304 if backup:
305 305 node = tostrip[0]
306 306 backupfile = _createstripbackup(repo, tostrip, node, topic)
307 307
308 308 with repo.transaction(b'strip') as tr:
309 309 phases.retractboundary(repo, tr, phases.archived, tostrip)
310 310 bmchanges = [(m, repo[newbmtarget].node()) for m in updatebm]
311 311 repo._bookmarks.applychanges(repo, tr, bmchanges)
312 312 return backupfile
313 313
314 314
315 315 def _bookmarkmovements(repo, tostrip):
316 316 # compute necessary bookmark movement
317 317 bm = repo._bookmarks
318 318 updatebm = []
319 319 for m in bm:
320 320 rev = repo[bm[m]].rev()
321 321 if rev in tostrip:
322 322 updatebm.append(m)
323 323 newbmtarget = None
324 324 # If we need to move bookmarks, compute bookmark
325 325 # targets. Otherwise we can skip doing this logic.
326 326 if updatebm:
327 327 # For a set s, max(parents(s) - s) is the same as max(heads(::s - s)),
328 328 # but is much faster
329 329 newbmtarget = repo.revs(b'max(parents(%ld) - (%ld))', tostrip, tostrip)
330 330 if newbmtarget:
331 331 newbmtarget = repo[newbmtarget.first()].node()
332 332 else:
333 333 newbmtarget = b'.'
334 334 return newbmtarget, updatebm
335 335
336 336
337 337 def _createstripbackup(repo, stripbases, node, topic):
338 338 # backup the changeset we are about to strip
339 339 vfs = repo.vfs
340 340 cl = repo.changelog
341 341 backupfile = backupbundle(repo, stripbases, cl.heads(), node, topic)
342 342 repo.ui.status(_(b"saved backup bundle to %s\n") % vfs.join(backupfile))
343 343 repo.ui.log(
344 344 b"backupbundle", b"saved backup bundle to %s\n", vfs.join(backupfile)
345 345 )
346 346 return backupfile
347 347
348 348
349 349 def safestriproots(ui, repo, nodes):
350 350 """return list of roots of nodes where descendants are covered by nodes"""
351 351 torev = repo.unfiltered().changelog.rev
352 352 revs = set(torev(n) for n in nodes)
353 353 # tostrip = wanted - unsafe = wanted - ancestors(orphaned)
354 354 # orphaned = affected - wanted
355 355 # affected = descendants(roots(wanted))
356 356 # wanted = revs
357 357 revset = b'%ld - ( ::( (roots(%ld):: and not _phase(%s)) -%ld) )'
358 358 tostrip = set(repo.revs(revset, revs, revs, phases.internal, revs))
359 359 notstrip = revs - tostrip
360 360 if notstrip:
361 361 nodestr = b', '.join(sorted(short(repo[n].node()) for n in notstrip))
362 362 ui.warn(
363 363 _(b'warning: orphaned descendants detected, not stripping %s\n')
364 364 % nodestr
365 365 )
366 366 return [c.node() for c in repo.set(b'roots(%ld)', tostrip)]
367 367
368 368
369 369 class stripcallback(object):
370 370 """used as a transaction postclose callback"""
371 371
372 372 def __init__(self, ui, repo, backup, topic):
373 373 self.ui = ui
374 374 self.repo = repo
375 375 self.backup = backup
376 376 self.topic = topic or b'backup'
377 377 self.nodelist = []
378 378
379 379 def addnodes(self, nodes):
380 380 self.nodelist.extend(nodes)
381 381
382 382 def __call__(self, tr):
383 383 roots = safestriproots(self.ui, self.repo, self.nodelist)
384 384 if roots:
385 385 strip(self.ui, self.repo, roots, self.backup, self.topic)
386 386
387 387
388 388 def delayedstrip(ui, repo, nodelist, topic=None, backup=True):
389 389 """like strip, but works inside transaction and won't strip irreverent revs
390 390
391 391 nodelist must explicitly contain all descendants. Otherwise a warning will
392 392 be printed that some nodes are not stripped.
393 393
394 394 Will do a backup if `backup` is True. The last non-None "topic" will be
395 395 used as the backup topic name. The default backup topic name is "backup".
396 396 """
397 397 tr = repo.currenttransaction()
398 398 if not tr:
399 399 nodes = safestriproots(ui, repo, nodelist)
400 400 return strip(ui, repo, nodes, backup=backup, topic=topic)
401 401 # transaction postclose callbacks are called in alphabet order.
402 402 # use '\xff' as prefix so we are likely to be called last.
403 403 callback = tr.getpostclose(b'\xffstrip')
404 404 if callback is None:
405 405 callback = stripcallback(ui, repo, backup=backup, topic=topic)
406 406 tr.addpostclose(b'\xffstrip', callback)
407 407 if topic:
408 408 callback.topic = topic
409 409 callback.addnodes(nodelist)
410 410
411 411
412 412 def stripmanifest(repo, striprev, tr, files):
413 413 for revlog in manifestrevlogs(repo):
414 414 revlog.strip(striprev, tr)
415 415
416 416
417 417 def manifestrevlogs(repo):
418 418 yield repo.manifestlog.getstorage(b'')
419 419 if b'treemanifest' in repo.requirements:
420 420 # This logic is safe if treemanifest isn't enabled, but also
421 421 # pointless, so we skip it if treemanifest isn't enabled.
422 422 for unencoded, encoded, size in repo.store.datafiles():
423 423 if unencoded.startswith(b'meta/') and unencoded.endswith(
424 424 b'00manifest.i'
425 425 ):
426 426 dir = unencoded[5:-12]
427 427 yield repo.manifestlog.getstorage(dir)
428 428
429 429
430 430 def rebuildfncache(ui, repo):
431 431 """Rebuilds the fncache file from repo history.
432 432
433 433 Missing entries will be added. Extra entries will be removed.
434 434 """
435 435 repo = repo.unfiltered()
436 436
437 437 if b'fncache' not in repo.requirements:
438 438 ui.warn(
439 439 _(
440 440 b'(not rebuilding fncache because repository does not '
441 441 b'support fncache)\n'
442 442 )
443 443 )
444 444 return
445 445
446 446 with repo.lock():
447 447 fnc = repo.store.fncache
448 448 fnc.ensureloaded(warn=ui.warn)
449 449
450 450 oldentries = set(fnc.entries)
451 451 newentries = set()
452 452 seenfiles = set()
453 453
454 454 progress = ui.makeprogress(
455 455 _(b'rebuilding'), unit=_(b'changesets'), total=len(repo)
456 456 )
457 457 for rev in repo:
458 458 progress.update(rev)
459 459
460 460 ctx = repo[rev]
461 461 for f in ctx.files():
462 462 # This is to minimize I/O.
463 463 if f in seenfiles:
464 464 continue
465 465 seenfiles.add(f)
466 466
467 467 i = b'data/%s.i' % f
468 468 d = b'data/%s.d' % f
469 469
470 470 if repo.store._exists(i):
471 471 newentries.add(i)
472 472 if repo.store._exists(d):
473 473 newentries.add(d)
474 474
475 475 progress.complete()
476 476
477 477 if b'treemanifest' in repo.requirements:
478 478 # This logic is safe if treemanifest isn't enabled, but also
479 479 # pointless, so we skip it if treemanifest isn't enabled.
480 480 for dir in pathutil.dirs(seenfiles):
481 481 i = b'meta/%s/00manifest.i' % dir
482 482 d = b'meta/%s/00manifest.d' % dir
483 483
484 484 if repo.store._exists(i):
485 485 newentries.add(i)
486 486 if repo.store._exists(d):
487 487 newentries.add(d)
488 488
489 489 addcount = len(newentries - oldentries)
490 490 removecount = len(oldentries - newentries)
491 491 for p in sorted(oldentries - newentries):
492 492 ui.write(_(b'removing %s\n') % p)
493 493 for p in sorted(newentries - oldentries):
494 494 ui.write(_(b'adding %s\n') % p)
495 495
496 496 if addcount or removecount:
497 497 ui.write(
498 498 _(b'%d items added, %d removed from fncache\n')
499 499 % (addcount, removecount)
500 500 )
501 501 fnc.entries = newentries
502 502 fnc._dirty = True
503 503
504 504 with repo.transaction(b'fncache') as tr:
505 505 fnc.write(tr)
506 506 else:
507 507 ui.write(_(b'fncache already up to date\n'))
508 508
509 509
510 510 def deleteobsmarkers(obsstore, indices):
511 511 """Delete some obsmarkers from obsstore and return how many were deleted
512 512
513 513 'indices' is a list of ints which are the indices
514 514 of the markers to be deleted.
515 515
516 516 Every invocation of this function completely rewrites the obsstore file,
517 517 skipping the markers we want to be removed. The new temporary file is
518 518 created, remaining markers are written there and on .close() this file
519 519 gets atomically renamed to obsstore, thus guaranteeing consistency."""
520 520 if not indices:
521 521 # we don't want to rewrite the obsstore with the same content
522 522 return
523 523
524 524 left = []
525 525 current = obsstore._all
526 526 n = 0
527 527 for i, m in enumerate(current):
528 528 if i in indices:
529 529 n += 1
530 530 continue
531 531 left.append(m)
532 532
533 533 newobsstorefile = obsstore.svfs(b'obsstore', b'w', atomictemp=True)
534 534 for bytes in obsolete.encodemarkers(left, True, obsstore._version):
535 535 newobsstorefile.write(bytes)
536 536 newobsstorefile.close()
537 537 return n
@@ -1,66 +1,66 b''
1 1 from __future__ import absolute_import
2 2
3 3 import os
4 4
5 5 from . import (
6 6 encoding,
7 7 pycompat,
8 8 util,
9 9 win32,
10 10 )
11 11
12 12 try:
13 13 import _winreg as winreg # pytype: disable=import-error
14 14
15 15 winreg.CloseKey
16 16 except ImportError:
17 17 # py2 only
18 18 import winreg # pytype: disable=import-error
19 19
20 20 # MS-DOS 'more' is the only pager available by default on Windows.
21 21 fallbackpager = b'more'
22 22
23 23
24 24 def systemrcpath():
25 25 '''return default os-specific hgrc search path'''
26 26 rcpath = []
27 27 filename = win32.executablepath()
28 28 # Use mercurial.ini found in directory with hg.exe
29 29 progrc = os.path.join(os.path.dirname(filename), b'mercurial.ini')
30 30 rcpath.append(progrc)
31 31 # Use hgrc.d found in directory with hg.exe
32 32 progrcd = os.path.join(os.path.dirname(filename), b'hgrc.d')
33 33 if os.path.isdir(progrcd):
34 34 for f, kind in util.listdir(progrcd):
35 35 if f.endswith(b'.rc'):
36 36 rcpath.append(os.path.join(progrcd, f))
37 37 # else look for a system rcpath in the registry
38 38 value = util.lookupreg(
39 39 b'SOFTWARE\\Mercurial', None, winreg.HKEY_LOCAL_MACHINE
40 40 )
41 if not isinstance(value, str) or not value:
41 if not isinstance(value, bytes) or not value:
42 42 return rcpath
43 43 value = util.localpath(value)
44 44 for p in value.split(pycompat.ospathsep):
45 45 if p.lower().endswith(b'mercurial.ini'):
46 46 rcpath.append(p)
47 47 elif os.path.isdir(p):
48 48 for f, kind in util.listdir(p):
49 49 if f.endswith(b'.rc'):
50 50 rcpath.append(os.path.join(p, f))
51 51 return rcpath
52 52
53 53
54 54 def userrcpath():
55 55 '''return os-specific hgrc search path to the user dir'''
56 56 home = os.path.expanduser(b'~')
57 57 path = [os.path.join(home, b'mercurial.ini'), os.path.join(home, b'.hgrc')]
58 58 userprofile = encoding.environ.get(b'USERPROFILE')
59 59 if userprofile and userprofile != home:
60 60 path.append(os.path.join(userprofile, b'mercurial.ini'))
61 61 path.append(os.path.join(userprofile, b'.hgrc'))
62 62 return path
63 63
64 64
65 65 def termsize(ui):
66 66 return win32.termsize()
@@ -1,316 +1,320 b''
1 1 $ cat >> $HGRCPATH << EOF
2 2 > [extensions]
3 3 > githelp =
4 4 > EOF
5 5
6 6 $ hg init repo
7 7 $ cd repo
8 8 $ echo foo > test_file
9 9 $ mkdir dir
10 10 $ echo foo > dir/file
11 11 $ echo foo > removed_file
12 12 $ echo foo > deleted_file
13 13 $ hg add -q .
14 14 $ hg commit -m 'bar'
15 15 $ hg bookmark both
16 16 $ touch both
17 17 $ touch untracked_file
18 18 $ hg remove removed_file
19 19 $ rm deleted_file
20 20
21 21 githelp on a single command should succeed
22 22 $ hg githelp -- commit
23 23 hg commit
24 24 $ hg githelp -- git commit
25 25 hg commit
26 26
27 27 githelp should fail nicely if we don't give it arguments
28 28 $ hg githelp
29 29 abort: missing git command - usage: hg githelp -- <git command>
30 30 [255]
31 31 $ hg githelp -- git
32 32 abort: missing git command - usage: hg githelp -- <git command>
33 33 [255]
34 34
35 35 githelp on a command with options should succeed
36 36 $ hg githelp -- commit -pm "abc"
37 37 hg commit --interactive -m 'abc'
38 38
39 39 githelp on a command with standalone unrecognized option should succeed with warning
40 40 $ hg githelp -- commit -p -v
41 41 ignoring unknown option -v
42 42 hg commit --interactive
43 43
44 44 githelp on a command with unrecognized option packed with other options should fail with error
45 45 $ hg githelp -- commit -pv
46 46 abort: unknown option 'v' packed with other options
47 47 (please try passing the option as its own flag: -v)
48 48 [255]
49 49
50 50 githelp for git rebase --skip
51 51 $ hg githelp -- git rebase --skip
52 52 hg revert --all -r .
53 53 hg rebase --continue
54 54
55 55 githelp for git commit --amend (hg commit --amend pulls up an editor)
56 56 $ hg githelp -- commit --amend
57 57 hg commit --amend
58 58
59 59 githelp for git commit --amend --no-edit (hg amend does not pull up an editor)
60 60 $ hg githelp -- commit --amend --no-edit
61 61 hg amend
62 62
63 63 githelp for git checkout -- . (checking out a directory)
64 64 $ hg githelp -- checkout -- .
65 65 note: use --no-backup to avoid creating .orig files
66 66
67 67 hg revert .
68 68
69 69 githelp for git checkout "HEAD^" (should still work to pass a rev)
70 70 $ hg githelp -- checkout "HEAD^"
71 71 hg update .^
72 72
73 73 githelp checkout: args after -- should be treated as paths no matter what
74 74 $ hg githelp -- checkout -- HEAD
75 75 note: use --no-backup to avoid creating .orig files
76 76
77 77 hg revert HEAD
78 78
79 79 githelp for git checkout with rev and path
80 80 $ hg githelp -- checkout "HEAD^" -- file.txt
81 81 note: use --no-backup to avoid creating .orig files
82 82
83 83 hg revert -r .^ file.txt
84 84
85 85 githelp for git with rev and path, without separator
86 86 $ hg githelp -- checkout "HEAD^" file.txt
87 87 note: use --no-backup to avoid creating .orig files
88 88
89 89 hg revert -r .^ file.txt
90 90
91 91 githelp for checkout with a file as first argument
92 92 $ hg githelp -- checkout test_file
93 93 note: use --no-backup to avoid creating .orig files
94 94
95 95 hg revert test_file
96 96
97 97 githelp for checkout with a removed file as first argument
98 98 $ hg githelp -- checkout removed_file
99 99 note: use --no-backup to avoid creating .orig files
100 100
101 101 hg revert removed_file
102 102
103 103 githelp for checkout with a deleted file as first argument
104 104 $ hg githelp -- checkout deleted_file
105 105 note: use --no-backup to avoid creating .orig files
106 106
107 107 hg revert deleted_file
108 108
109 109 githelp for checkout with a untracked file as first argument
110 110 $ hg githelp -- checkout untracked_file
111 111 note: use --no-backup to avoid creating .orig files
112 112
113 113 hg revert untracked_file
114 114
115 115 githelp for checkout with a directory as first argument
116 116 $ hg githelp -- checkout dir
117 117 note: use --no-backup to avoid creating .orig files
118 118
119 119 hg revert dir
120 120
121 121 githelp for checkout when not in repo root
122 122 $ cd dir
123 123 $ hg githelp -- checkout file
124 124 note: use --no-backup to avoid creating .orig files
125 125
126 126 hg revert file
127 127
128 128 $ cd ..
129 129
130 130 githelp for checkout with an argument that is both a file and a revision
131 131 $ hg githelp -- checkout both
132 132 hg update both
133 133
134 134 githelp for checkout with the -p option
135 135 $ hg githelp -- git checkout -p xyz
136 136 hg revert -i -r xyz
137 137
138 138 $ hg githelp -- git checkout -p xyz -- abc
139 139 note: use --no-backup to avoid creating .orig files
140 140
141 141 hg revert -i -r xyz abc
142 142
143 143 githelp for checkout with the -f option and a rev
144 144 $ hg githelp -- git checkout -f xyz
145 145 hg update -C xyz
146 146 $ hg githelp -- git checkout --force xyz
147 147 hg update -C xyz
148 148
149 149 githelp for checkout with the -f option without an arg
150 150 $ hg githelp -- git checkout -f
151 151 hg revert --all
152 152 $ hg githelp -- git checkout --force
153 153 hg revert --all
154 154
155 155 githelp for grep with pattern and path
156 156 $ hg githelp -- grep shrubbery flib/intern/
157 157 hg grep shrubbery flib/intern/
158 158
159 159 githelp for reset, checking ~ in git becomes ~1 in mercurial
160 160 $ hg githelp -- reset HEAD~
161 161 hg update .~1
162 162 $ hg githelp -- reset "HEAD^"
163 163 hg update .^
164 164 $ hg githelp -- reset HEAD~3
165 165 hg update .~3
166 166
167 167 $ hg githelp -- reset --mixed HEAD
168 168 note: --mixed has no meaning since Mercurial has no staging area
169 169
170 170 hg update .
171 171 $ hg githelp -- reset --soft HEAD
172 172 note: --soft has no meaning since Mercurial has no staging area
173 173
174 174 hg update .
175 175 $ hg githelp -- reset --hard HEAD
176 176 hg update --clean .
177 177
178 178 githelp for git show --name-status
179 179 $ hg githelp -- git show --name-status
180 180 hg log --style status -r .
181 181
182 182 githelp for git show --pretty=format: --name-status
183 183 $ hg githelp -- git show --pretty=format: --name-status
184 184 hg status --change .
185 185
186 186 githelp for show with no arguments
187 187 $ hg githelp -- show
188 188 hg export
189 189
190 190 githelp for show with a path
191 191 $ hg githelp -- show test_file
192 192 hg cat test_file
193 193
194 194 githelp for show with not a path:
195 195 $ hg githelp -- show rev
196 196 hg export rev
197 197
198 198 githelp for show with many arguments
199 199 $ hg githelp -- show argone argtwo
200 200 hg export argone argtwo
201 201 $ hg githelp -- show test_file argone argtwo
202 202 hg cat test_file argone argtwo
203 203
204 204 githelp for show with --unified options
205 205 $ hg githelp -- show --unified=10
206 206 hg export --config diff.unified=10
207 207 $ hg githelp -- show -U100
208 208 hg export --config diff.unified=100
209 209
210 210 githelp for show with a path and --unified
211 211 $ hg githelp -- show -U20 test_file
212 212 hg cat test_file --config diff.unified=20
213 213
214 214 githelp for stash drop without name
215 215 $ hg githelp -- git stash drop
216 216 hg shelve -d <shelve name>
217 217
218 218 githelp for stash drop with name
219 219 $ hg githelp -- git stash drop xyz
220 220 hg shelve -d xyz
221 221
222 222 githelp for stash list with patch
223 223 $ hg githelp -- git stash list -p
224 224 hg shelve -l -p
225 225
226 226 githelp for stash show
227 227 $ hg githelp -- git stash show
228 228 hg shelve --stat
229 229
230 230 githelp for stash show with patch and name
231 231 $ hg githelp -- git stash show -p mystash
232 232 hg shelve -p mystash
233 233
234 234 githelp for stash clear
235 235 $ hg githelp -- git stash clear
236 236 hg shelve --cleanup
237 237
238 238 githelp for whatchanged should show deprecated message
239 239 $ hg githelp -- whatchanged -p
240 240 this command has been deprecated in the git project, thus isn't supported by this tool
241 241
242 242
243 243 githelp for git branch -m renaming
244 244 $ hg githelp -- git branch -m old new
245 245 hg bookmark -m old new
246 246
247 247 When the old name is omitted, git branch -m new renames the current branch.
248 248 $ hg githelp -- git branch -m new
249 249 hg bookmark -m `hg log -T"{activebookmark}" -r .` new
250 250
251 251 Branch deletion in git strips commits
252 252 $ hg githelp -- git branch -d
253 253 hg strip -B
254 254 $ hg githelp -- git branch -d feature
255 255 hg strip -B feature -B
256 256 $ hg githelp -- git branch --delete experiment1 experiment2
257 257 hg strip -B experiment1 -B experiment2 -B
258 258
259 259 githelp for reuse message using the shorthand
260 260 $ hg githelp -- git commit -C deadbeef
261 261 hg commit -M deadbeef
262 262
263 263 githelp for reuse message using the the long version
264 264 $ hg githelp -- git commit --reuse-message deadbeef
265 265 hg commit -M deadbeef
266 266
267 githelp for reuse message using HEAD
268 $ hg githelp -- git commit --reuse-message HEAD~
269 hg commit -M .~1
270
267 271 githelp for apply with no options
268 272 $ hg githelp -- apply
269 273 hg import --no-commit
270 274
271 275 githelp for apply with directory strip custom
272 276 $ hg githelp -- apply -p 5
273 277 hg import --no-commit -p 5
274 278
275 279 githelp for apply with prefix directory
276 280 $ hg githelp -- apply --directory=modules
277 281 hg import --no-commit --prefix modules
278 282
279 283 git merge-base
280 284 $ hg githelp -- git merge-base --is-ancestor
281 285 ignoring unknown option --is-ancestor
282 286 note: ancestors() is part of the revset language
283 287 (learn more about revsets with 'hg help revsets')
284 288
285 289 hg log -T '{node}\n' -r 'ancestor(A,B)'
286 290
287 291 githelp for git blame
288 292 $ hg githelp -- git blame
289 293 hg annotate -udl
290 294
291 295 githelp for add
292 296
293 297 $ hg githelp -- git add
294 298 hg add
295 299
296 300 $ hg githelp -- git add -p
297 301 note: Mercurial will commit when complete, as there is no staging area in Mercurial
298 302
299 303 hg commit --interactive
300 304
301 305 $ hg githelp -- git add --all
302 306 note: use hg addremove to remove files that have been deleted
303 307
304 308 hg add
305 309
306 310 githelp for reflog
307 311
308 312 $ hg githelp -- git reflog
309 313 hg journal
310 314
311 315 note: in hg commits can be deleted from repo but we always have backups
312 316
313 317 $ hg githelp -- git reflog --all
314 318 hg journal --all
315 319
316 320 note: in hg commits can be deleted from repo but we always have backups
@@ -1,1028 +1,1153 b''
1 1 $ cat >> $HGRCPATH << EOF
2 2 > [diff]
3 3 > git = true
4 4 > EOF
5 5
6 6 $ hg init
7 7 $ cat > foo << EOF
8 8 > 0
9 9 > 1
10 10 > 2
11 11 > 3
12 12 > 4
13 13 > EOF
14 14 $ hg ci -Am init
15 15 adding foo
16 16 $ cat > foo << EOF
17 17 > 0
18 18 > 0
19 19 > 0
20 20 > 0
21 21 > 1
22 22 > 2
23 23 > 3
24 24 > 4
25 25 > EOF
26 26 $ hg ci -m 'more 0'
27 27 $ sed 's/2/2+/' foo > foo.new
28 28 $ mv foo.new foo
29 29 $ cat > bar << EOF
30 30 > a
31 31 > b
32 32 > c
33 33 > d
34 34 > e
35 35 > EOF
36 36 $ hg add bar
37 37 $ hg ci -Am "2 -> 2+; added bar"
38 38 $ cat >> foo << EOF
39 39 > 5
40 40 > 6
41 41 > 7
42 42 > 8
43 43 > 9
44 44 > 10
45 45 > 11
46 46 > EOF
47 47 $ hg ci -m "to 11"
48 48
49 49 Add some changes with two diff hunks
50 50
51 51 $ sed 's/^1$/ 1/' foo > foo.new
52 52 $ mv foo.new foo
53 53 $ sed 's/^11$/11+/' foo > foo.new
54 54 $ mv foo.new foo
55 55 $ hg ci -m '11 -> 11+; leading space before "1"'
56 56 (make sure there are two hunks in "foo")
57 57 $ hg diff -c .
58 58 diff --git a/foo b/foo
59 59 --- a/foo
60 60 +++ b/foo
61 61 @@ -2,7 +2,7 @@
62 62 0
63 63 0
64 64 0
65 65 -1
66 66 + 1
67 67 2+
68 68 3
69 69 4
70 70 @@ -12,4 +12,4 @@
71 71 8
72 72 9
73 73 10
74 74 -11
75 75 +11+
76 76 $ sed 's/3/3+/' foo > foo.new
77 77 $ mv foo.new foo
78 78 $ sed 's/^11+$/11-/' foo > foo.new
79 79 $ mv foo.new foo
80 80 $ sed 's/a/a+/' bar > bar.new
81 81 $ mv bar.new bar
82 82 $ hg ci -m 'foo: 3 -> 3+ and 11+ -> 11-; bar: a -> a+'
83 83 (make sure there are two hunks in "foo")
84 84 $ hg diff -c . foo
85 85 diff --git a/foo b/foo
86 86 --- a/foo
87 87 +++ b/foo
88 88 @@ -4,7 +4,7 @@
89 89 0
90 90 1
91 91 2+
92 92 -3
93 93 +3+
94 94 4
95 95 5
96 96 6
97 97 @@ -12,4 +12,4 @@
98 98 8
99 99 9
100 100 10
101 101 -11+
102 102 +11-
103 103
104 104 $ hg log -f -L foo,5:7 -p
105 105 changeset: 5:cfdf972b3971
106 106 tag: tip
107 107 user: test
108 108 date: Thu Jan 01 00:00:00 1970 +0000
109 109 summary: foo: 3 -> 3+ and 11+ -> 11-; bar: a -> a+
110 110
111 111 diff --git a/foo b/foo
112 112 --- a/foo
113 113 +++ b/foo
114 114 @@ -4,7 +4,7 @@
115 115 0
116 116 1
117 117 2+
118 118 -3
119 119 +3+
120 120 4
121 121 5
122 122 6
123 123
124 124 changeset: 4:eaec41c1a0c9
125 125 user: test
126 126 date: Thu Jan 01 00:00:00 1970 +0000
127 127 summary: 11 -> 11+; leading space before "1"
128 128
129 129 diff --git a/foo b/foo
130 130 --- a/foo
131 131 +++ b/foo
132 132 @@ -2,7 +2,7 @@
133 133 0
134 134 0
135 135 0
136 136 -1
137 137 + 1
138 138 2+
139 139 3
140 140 4
141 141
142 142 changeset: 2:63a884426fd0
143 143 user: test
144 144 date: Thu Jan 01 00:00:00 1970 +0000
145 145 summary: 2 -> 2+; added bar
146 146
147 147 diff --git a/foo b/foo
148 148 --- a/foo
149 149 +++ b/foo
150 150 @@ -3,6 +3,6 @@
151 151 0
152 152 0
153 153 1
154 154 -2
155 155 +2+
156 156 3
157 157 4
158 158
159 159 changeset: 0:5ae1f82b9a00
160 160 user: test
161 161 date: Thu Jan 01 00:00:00 1970 +0000
162 162 summary: init
163 163
164 164 diff --git a/foo b/foo
165 165 new file mode 100644
166 166 --- /dev/null
167 167 +++ b/foo
168 168 @@ -0,0 +1,5 @@
169 169 +0
170 170 +1
171 171 +2
172 172 +3
173 173 +4
174 174
175 175 $ hg log -f --graph -L foo,5:7 -p
176 176 @ changeset: 5:cfdf972b3971
177 177 | tag: tip
178 178 | user: test
179 179 | date: Thu Jan 01 00:00:00 1970 +0000
180 180 | summary: foo: 3 -> 3+ and 11+ -> 11-; bar: a -> a+
181 181 |
182 182 | diff --git a/foo b/foo
183 183 | --- a/foo
184 184 | +++ b/foo
185 185 | @@ -4,7 +4,7 @@
186 186 | 0
187 187 | 1
188 188 | 2+
189 189 | -3
190 190 | +3+
191 191 | 4
192 192 | 5
193 193 | 6
194 194 |
195 195 o changeset: 4:eaec41c1a0c9
196 196 : user: test
197 197 : date: Thu Jan 01 00:00:00 1970 +0000
198 198 : summary: 11 -> 11+; leading space before "1"
199 199 :
200 200 : diff --git a/foo b/foo
201 201 : --- a/foo
202 202 : +++ b/foo
203 203 : @@ -2,7 +2,7 @@
204 204 : 0
205 205 : 0
206 206 : 0
207 207 : -1
208 208 : + 1
209 209 : 2+
210 210 : 3
211 211 : 4
212 212 :
213 213 o changeset: 2:63a884426fd0
214 214 : user: test
215 215 : date: Thu Jan 01 00:00:00 1970 +0000
216 216 : summary: 2 -> 2+; added bar
217 217 :
218 218 : diff --git a/foo b/foo
219 219 : --- a/foo
220 220 : +++ b/foo
221 221 : @@ -3,6 +3,6 @@
222 222 : 0
223 223 : 0
224 224 : 1
225 225 : -2
226 226 : +2+
227 227 : 3
228 228 : 4
229 229 :
230 230 o changeset: 0:5ae1f82b9a00
231 231 user: test
232 232 date: Thu Jan 01 00:00:00 1970 +0000
233 233 summary: init
234 234
235 235 diff --git a/foo b/foo
236 236 new file mode 100644
237 237 --- /dev/null
238 238 +++ b/foo
239 239 @@ -0,0 +1,5 @@
240 240 +0
241 241 +1
242 242 +2
243 243 +3
244 244 +4
245 245
246 246
247 247 With --template.
248 248
249 249 $ hg log -f -L foo,5:7 -T '{rev}:{node|short} {desc|firstline}\n'
250 250 5:cfdf972b3971 foo: 3 -> 3+ and 11+ -> 11-; bar: a -> a+
251 251 4:eaec41c1a0c9 11 -> 11+; leading space before "1"
252 252 2:63a884426fd0 2 -> 2+; added bar
253 253 0:5ae1f82b9a00 init
254 254 $ hg log -f -L foo,5:7 -T json
255 255 [
256 256 {
257 257 "bookmarks": [],
258 258 "branch": "default",
259 259 "date": [0, 0],
260 260 "desc": "foo: 3 -> 3+ and 11+ -> 11-; bar: a -> a+",
261 261 "node": "cfdf972b3971a2a59638bf9583c0debbffee5404",
262 262 "parents": ["eaec41c1a0c9ad0a5e999611d0149d171beffb8c"],
263 263 "phase": "draft",
264 264 "rev": 5,
265 265 "tags": ["tip"],
266 266 "user": "test"
267 267 },
268 268 {
269 269 "bookmarks": [],
270 270 "branch": "default",
271 271 "date": [0, 0],
272 272 "desc": "11 -> 11+; leading space before \"1\"",
273 273 "node": "eaec41c1a0c9ad0a5e999611d0149d171beffb8c",
274 274 "parents": ["730a61fbaecf426c17c2c66bc42d195b5d5b0ba8"],
275 275 "phase": "draft",
276 276 "rev": 4,
277 277 "tags": [],
278 278 "user": "test"
279 279 },
280 280 {
281 281 "bookmarks": [],
282 282 "branch": "default",
283 283 "date": [0, 0],
284 284 "desc": "2 -> 2+; added bar",
285 285 "node": "63a884426fd0b277fcd55895bbb2f230434576eb",
286 286 "parents": ["29a1e7c6b80024f63f310a2d71de979e9d2996d7"],
287 287 "phase": "draft",
288 288 "rev": 2,
289 289 "tags": [],
290 290 "user": "test"
291 291 },
292 292 {
293 293 "bookmarks": [],
294 294 "branch": "default",
295 295 "date": [0, 0],
296 296 "desc": "init",
297 297 "node": "5ae1f82b9a000ff1e0967d0dac1c58b9d796e1b4",
298 298 "parents": ["0000000000000000000000000000000000000000"],
299 299 "phase": "draft",
300 300 "rev": 0,
301 301 "tags": [],
302 302 "user": "test"
303 303 }
304 304 ]
305 305
306 306 With some white-space diff option, respective revisions are skipped.
307 307
308 308 $ hg log -f -L foo,5:7 -p --config diff.ignorews=true
309 309 changeset: 5:cfdf972b3971
310 310 tag: tip
311 311 user: test
312 312 date: Thu Jan 01 00:00:00 1970 +0000
313 313 summary: foo: 3 -> 3+ and 11+ -> 11-; bar: a -> a+
314 314
315 315 diff --git a/foo b/foo
316 316 --- a/foo
317 317 +++ b/foo
318 318 @@ -4,7 +4,7 @@
319 319 0
320 320 1
321 321 2+
322 322 -3
323 323 +3+
324 324 4
325 325 5
326 326 6
327 327
328 328 changeset: 2:63a884426fd0
329 329 user: test
330 330 date: Thu Jan 01 00:00:00 1970 +0000
331 331 summary: 2 -> 2+; added bar
332 332
333 333 diff --git a/foo b/foo
334 334 --- a/foo
335 335 +++ b/foo
336 336 @@ -3,6 +3,6 @@
337 337 0
338 338 0
339 339 1
340 340 -2
341 341 +2+
342 342 3
343 343 4
344 344
345 345 changeset: 0:5ae1f82b9a00
346 346 user: test
347 347 date: Thu Jan 01 00:00:00 1970 +0000
348 348 summary: init
349 349
350 350 diff --git a/foo b/foo
351 351 new file mode 100644
352 352 --- /dev/null
353 353 +++ b/foo
354 354 @@ -0,0 +1,5 @@
355 355 +0
356 356 +1
357 357 +2
358 358 +3
359 359 +4
360 360
361 361
362 362 Regular file patterns are not allowed.
363 363
364 364 $ hg log -f -L foo,5:7 -p bar
365 365 abort: FILE arguments are not compatible with --line-range option
366 366 [255]
367 367
368 368 Option --rev acts as a restriction.
369 369
370 370 $ hg log -f -L foo,5:7 -p -r 'desc(2)'
371 371 changeset: 2:63a884426fd0
372 372 user: test
373 373 date: Thu Jan 01 00:00:00 1970 +0000
374 374 summary: 2 -> 2+; added bar
375 375
376 376 diff --git a/foo b/foo
377 377 --- a/foo
378 378 +++ b/foo
379 379 @@ -3,6 +3,6 @@
380 380 0
381 381 0
382 382 1
383 383 -2
384 384 +2+
385 385 3
386 386 4
387 387
388 388 changeset: 0:5ae1f82b9a00
389 389 user: test
390 390 date: Thu Jan 01 00:00:00 1970 +0000
391 391 summary: init
392 392
393 393 diff --git a/foo b/foo
394 394 new file mode 100644
395 395 --- /dev/null
396 396 +++ b/foo
397 397 @@ -0,0 +1,5 @@
398 398 +0
399 399 +1
400 400 +2
401 401 +3
402 402 +4
403 403
404 404
405 405 With several -L patterns, changes touching any files in their respective line
406 406 range are show.
407 407
408 408 $ hg log -f -L foo,5:7 -L bar,1:2 -p
409 409 changeset: 5:cfdf972b3971
410 410 tag: tip
411 411 user: test
412 412 date: Thu Jan 01 00:00:00 1970 +0000
413 413 summary: foo: 3 -> 3+ and 11+ -> 11-; bar: a -> a+
414 414
415 415 diff --git a/bar b/bar
416 416 --- a/bar
417 417 +++ b/bar
418 418 @@ -1,4 +1,4 @@
419 419 -a
420 420 +a+
421 421 b
422 422 c
423 423 d
424 424 diff --git a/foo b/foo
425 425 --- a/foo
426 426 +++ b/foo
427 427 @@ -4,7 +4,7 @@
428 428 0
429 429 1
430 430 2+
431 431 -3
432 432 +3+
433 433 4
434 434 5
435 435 6
436 436
437 437 changeset: 4:eaec41c1a0c9
438 438 user: test
439 439 date: Thu Jan 01 00:00:00 1970 +0000
440 440 summary: 11 -> 11+; leading space before "1"
441 441
442 442 diff --git a/foo b/foo
443 443 --- a/foo
444 444 +++ b/foo
445 445 @@ -2,7 +2,7 @@
446 446 0
447 447 0
448 448 0
449 449 -1
450 450 + 1
451 451 2+
452 452 3
453 453 4
454 454
455 455 changeset: 2:63a884426fd0
456 456 user: test
457 457 date: Thu Jan 01 00:00:00 1970 +0000
458 458 summary: 2 -> 2+; added bar
459 459
460 460 diff --git a/bar b/bar
461 461 new file mode 100644
462 462 --- /dev/null
463 463 +++ b/bar
464 464 @@ -0,0 +1,5 @@
465 465 +a
466 466 +b
467 467 +c
468 468 +d
469 469 +e
470 470 diff --git a/foo b/foo
471 471 --- a/foo
472 472 +++ b/foo
473 473 @@ -3,6 +3,6 @@
474 474 0
475 475 0
476 476 1
477 477 -2
478 478 +2+
479 479 3
480 480 4
481 481
482 482 changeset: 0:5ae1f82b9a00
483 483 user: test
484 484 date: Thu Jan 01 00:00:00 1970 +0000
485 485 summary: init
486 486
487 487 diff --git a/foo b/foo
488 488 new file mode 100644
489 489 --- /dev/null
490 490 +++ b/foo
491 491 @@ -0,0 +1,5 @@
492 492 +0
493 493 +1
494 494 +2
495 495 +3
496 496 +4
497 497
498 498
499 499 Multiple -L options with the same file yields changes touching any of
500 500 specified line ranges.
501 501
502 502 $ hg log -f -L foo,5:7 -L foo,14:15 -p
503 503 changeset: 5:cfdf972b3971
504 504 tag: tip
505 505 user: test
506 506 date: Thu Jan 01 00:00:00 1970 +0000
507 507 summary: foo: 3 -> 3+ and 11+ -> 11-; bar: a -> a+
508 508
509 509 diff --git a/foo b/foo
510 510 --- a/foo
511 511 +++ b/foo
512 512 @@ -4,7 +4,7 @@
513 513 0
514 514 1
515 515 2+
516 516 -3
517 517 +3+
518 518 4
519 519 5
520 520 6
521 521 @@ -12,4 +12,4 @@
522 522 8
523 523 9
524 524 10
525 525 -11+
526 526 +11-
527 527
528 528 changeset: 4:eaec41c1a0c9
529 529 user: test
530 530 date: Thu Jan 01 00:00:00 1970 +0000
531 531 summary: 11 -> 11+; leading space before "1"
532 532
533 533 diff --git a/foo b/foo
534 534 --- a/foo
535 535 +++ b/foo
536 536 @@ -2,7 +2,7 @@
537 537 0
538 538 0
539 539 0
540 540 -1
541 541 + 1
542 542 2+
543 543 3
544 544 4
545 545 @@ -12,4 +12,4 @@
546 546 8
547 547 9
548 548 10
549 549 -11
550 550 +11+
551 551
552 552 changeset: 3:730a61fbaecf
553 553 user: test
554 554 date: Thu Jan 01 00:00:00 1970 +0000
555 555 summary: to 11
556 556
557 557 diff --git a/foo b/foo
558 558 --- a/foo
559 559 +++ b/foo
560 560 @@ -6,3 +6,10 @@
561 561 2+
562 562 3
563 563 4
564 564 +5
565 565 +6
566 566 +7
567 567 +8
568 568 +9
569 569 +10
570 570 +11
571 571
572 572 changeset: 2:63a884426fd0
573 573 user: test
574 574 date: Thu Jan 01 00:00:00 1970 +0000
575 575 summary: 2 -> 2+; added bar
576 576
577 577 diff --git a/foo b/foo
578 578 --- a/foo
579 579 +++ b/foo
580 580 @@ -3,6 +3,6 @@
581 581 0
582 582 0
583 583 1
584 584 -2
585 585 +2+
586 586 3
587 587 4
588 588
589 589 changeset: 0:5ae1f82b9a00
590 590 user: test
591 591 date: Thu Jan 01 00:00:00 1970 +0000
592 592 summary: init
593 593
594 594 diff --git a/foo b/foo
595 595 new file mode 100644
596 596 --- /dev/null
597 597 +++ b/foo
598 598 @@ -0,0 +1,5 @@
599 599 +0
600 600 +1
601 601 +2
602 602 +3
603 603 +4
604 604
605 605
606 606 A file with a comma in its name.
607 607
608 608 $ cat > ba,z << EOF
609 609 > q
610 610 > w
611 611 > e
612 612 > r
613 613 > t
614 614 > y
615 615 > EOF
616 616 $ hg ci -Am 'querty'
617 617 adding ba,z
618 618 $ cat >> ba,z << EOF
619 619 > u
620 620 > i
621 621 > o
622 622 > p
623 623 > EOF
624 624 $ hg ci -m 'more keys'
625 625 $ cat > ba,z << EOF
626 626 > a
627 627 > z
628 628 > e
629 629 > r
630 630 > t
631 631 > y
632 632 > u
633 633 > i
634 634 > o
635 635 > p
636 636 > EOF
637 637 $ hg ci -m 'azerty'
638 638 $ hg log -f -L ba,z,1:2 -p
639 639 changeset: 8:52373265138b
640 640 tag: tip
641 641 user: test
642 642 date: Thu Jan 01 00:00:00 1970 +0000
643 643 summary: azerty
644 644
645 645 diff --git a/ba,z b/ba,z
646 646 --- a/ba,z
647 647 +++ b/ba,z
648 648 @@ -1,5 +1,5 @@
649 649 -q
650 650 -w
651 651 +a
652 652 +z
653 653 e
654 654 r
655 655 t
656 656
657 657 changeset: 6:96ba8850f316
658 658 user: test
659 659 date: Thu Jan 01 00:00:00 1970 +0000
660 660 summary: querty
661 661
662 662 diff --git a/ba,z b/ba,z
663 663 new file mode 100644
664 664 --- /dev/null
665 665 +++ b/ba,z
666 666 @@ -0,0 +1,6 @@
667 667 +q
668 668 +w
669 669 +e
670 670 +r
671 671 +t
672 672 +y
673 673
674 674
675 675 Exact prefix kinds work in -L options.
676 676
677 677 $ mkdir dir
678 678 $ cd dir
679 679 $ hg log -f -L path:foo,5:7 -p
680 680 changeset: 5:cfdf972b3971
681 681 user: test
682 682 date: Thu Jan 01 00:00:00 1970 +0000
683 683 summary: foo: 3 -> 3+ and 11+ -> 11-; bar: a -> a+
684 684
685 685 diff --git a/foo b/foo
686 686 --- a/foo
687 687 +++ b/foo
688 688 @@ -4,7 +4,7 @@
689 689 0
690 690 1
691 691 2+
692 692 -3
693 693 +3+
694 694 4
695 695 5
696 696 6
697 697
698 698 changeset: 4:eaec41c1a0c9
699 699 user: test
700 700 date: Thu Jan 01 00:00:00 1970 +0000
701 701 summary: 11 -> 11+; leading space before "1"
702 702
703 703 diff --git a/foo b/foo
704 704 --- a/foo
705 705 +++ b/foo
706 706 @@ -2,7 +2,7 @@
707 707 0
708 708 0
709 709 0
710 710 -1
711 711 + 1
712 712 2+
713 713 3
714 714 4
715 715
716 716 changeset: 2:63a884426fd0
717 717 user: test
718 718 date: Thu Jan 01 00:00:00 1970 +0000
719 719 summary: 2 -> 2+; added bar
720 720
721 721 diff --git a/foo b/foo
722 722 --- a/foo
723 723 +++ b/foo
724 724 @@ -3,6 +3,6 @@
725 725 0
726 726 0
727 727 1
728 728 -2
729 729 +2+
730 730 3
731 731 4
732 732
733 733 changeset: 0:5ae1f82b9a00
734 734 user: test
735 735 date: Thu Jan 01 00:00:00 1970 +0000
736 736 summary: init
737 737
738 738 diff --git a/foo b/foo
739 739 new file mode 100644
740 740 --- /dev/null
741 741 +++ b/foo
742 742 @@ -0,0 +1,5 @@
743 743 +0
744 744 +1
745 745 +2
746 746 +3
747 747 +4
748 748
749 749
750 750 Renames are followed.
751 751
752 752 $ hg mv ../foo baz
753 753 $ sed 's/1/1+/' baz > baz.new
754 754 $ mv baz.new baz
755 755 $ hg ci -m 'foo -> dir/baz; 1-1+'
756 756 $ hg diff -c .
757 757 diff --git a/foo b/dir/baz
758 758 rename from foo
759 759 rename to dir/baz
760 760 --- a/foo
761 761 +++ b/dir/baz
762 762 @@ -2,7 +2,7 @@
763 763 0
764 764 0
765 765 0
766 766 - 1
767 767 + 1+
768 768 2+
769 769 3+
770 770 4
771 771 @@ -11,5 +11,5 @@
772 772 7
773 773 8
774 774 9
775 775 -10
776 776 -11-
777 777 +1+0
778 778 +1+1-
779 779 $ hg log -f -L relpath:baz,5:7 -p
780 780 changeset: 9:6af29c3a778f
781 781 tag: tip
782 782 user: test
783 783 date: Thu Jan 01 00:00:00 1970 +0000
784 784 summary: foo -> dir/baz; 1-1+
785 785
786 786 diff --git a/foo b/dir/baz
787 787 copy from foo
788 788 copy to dir/baz
789 789 --- a/foo
790 790 +++ b/dir/baz
791 791 @@ -2,7 +2,7 @@
792 792 0
793 793 0
794 794 0
795 795 - 1
796 796 + 1+
797 797 2+
798 798 3+
799 799 4
800 800
801 801 changeset: 5:cfdf972b3971
802 802 user: test
803 803 date: Thu Jan 01 00:00:00 1970 +0000
804 804 summary: foo: 3 -> 3+ and 11+ -> 11-; bar: a -> a+
805 805
806 806 diff --git a/foo b/foo
807 807 --- a/foo
808 808 +++ b/foo
809 809 @@ -4,7 +4,7 @@
810 810 0
811 811 1
812 812 2+
813 813 -3
814 814 +3+
815 815 4
816 816 5
817 817 6
818 818
819 819 changeset: 4:eaec41c1a0c9
820 820 user: test
821 821 date: Thu Jan 01 00:00:00 1970 +0000
822 822 summary: 11 -> 11+; leading space before "1"
823 823
824 824 diff --git a/foo b/foo
825 825 --- a/foo
826 826 +++ b/foo
827 827 @@ -2,7 +2,7 @@
828 828 0
829 829 0
830 830 0
831 831 -1
832 832 + 1
833 833 2+
834 834 3
835 835 4
836 836
837 837 changeset: 2:63a884426fd0
838 838 user: test
839 839 date: Thu Jan 01 00:00:00 1970 +0000
840 840 summary: 2 -> 2+; added bar
841 841
842 842 diff --git a/foo b/foo
843 843 --- a/foo
844 844 +++ b/foo
845 845 @@ -3,6 +3,6 @@
846 846 0
847 847 0
848 848 1
849 849 -2
850 850 +2+
851 851 3
852 852 4
853 853
854 854 changeset: 0:5ae1f82b9a00
855 855 user: test
856 856 date: Thu Jan 01 00:00:00 1970 +0000
857 857 summary: init
858 858
859 859 diff --git a/foo b/foo
860 860 new file mode 100644
861 861 --- /dev/null
862 862 +++ b/foo
863 863 @@ -0,0 +1,5 @@
864 864 +0
865 865 +1
866 866 +2
867 867 +3
868 868 +4
869 869
870 870
871 Uncommitted changes with a rename
872
873 $ hg mv baz bazn
874 $ hg log -f -L bazn,5:7
875 changeset: 9:6af29c3a778f
876 tag: tip
877 user: test
878 date: Thu Jan 01 00:00:00 1970 +0000
879 summary: foo -> dir/baz; 1-1+
880
881 changeset: 5:cfdf972b3971
882 user: test
883 date: Thu Jan 01 00:00:00 1970 +0000
884 summary: foo: 3 -> 3+ and 11+ -> 11-; bar: a -> a+
885
886 changeset: 4:eaec41c1a0c9
887 user: test
888 date: Thu Jan 01 00:00:00 1970 +0000
889 summary: 11 -> 11+; leading space before "1"
890
891 changeset: 2:63a884426fd0
892 user: test
893 date: Thu Jan 01 00:00:00 1970 +0000
894 summary: 2 -> 2+; added bar
895
896 changeset: 0:5ae1f82b9a00
897 user: test
898 date: Thu Jan 01 00:00:00 1970 +0000
899 summary: init
900
901
902 Uncommitted changes in requested line range
903
904 $ sed 's/2/ /' bazn > bazn.new
905 $ mv bazn.new bazn
906 $ hg diff
907 diff --git a/dir/baz b/dir/bazn
908 rename from dir/baz
909 rename to dir/bazn
910 --- a/dir/baz
911 +++ b/dir/bazn
912 @@ -3,7 +3,7 @@
913 0
914 0
915 1+
916 -2+
917 + +
918 3+
919 4
920 5
921 $ hg log -f -L bazn,5:7
922 changeset: 9:6af29c3a778f
923 tag: tip
924 user: test
925 date: Thu Jan 01 00:00:00 1970 +0000
926 summary: foo -> dir/baz; 1-1+
927
928 changeset: 5:cfdf972b3971
929 user: test
930 date: Thu Jan 01 00:00:00 1970 +0000
931 summary: foo: 3 -> 3+ and 11+ -> 11-; bar: a -> a+
932
933 changeset: 4:eaec41c1a0c9
934 user: test
935 date: Thu Jan 01 00:00:00 1970 +0000
936 summary: 11 -> 11+; leading space before "1"
937
938 changeset: 2:63a884426fd0
939 user: test
940 date: Thu Jan 01 00:00:00 1970 +0000
941 summary: 2 -> 2+; added bar
942
943 changeset: 0:5ae1f82b9a00
944 user: test
945 date: Thu Jan 01 00:00:00 1970 +0000
946 summary: init
947
948
949 Uncommitted changes in line-range + wdir()
950
951 $ hg log -r 'wdir()' -f -L bazn,5:7 --limit 2 -p
952 changeset: 2147483647:ffffffffffff
953 parent: 9:6af29c3a778f
954 user: test
955 date: Thu Jan 01 00:00:00 1970 +0000
956
957 diff --git a/dir/baz b/dir/bazn
958 copy from dir/baz
959 copy to dir/bazn
960 --- a/dir/baz
961 +++ b/dir/bazn
962 @@ -3,7 +3,7 @@
963 0
964 0
965 1+
966 -2+
967 + +
968 3+
969 4
970 5
971
972 changeset: 9:6af29c3a778f
973 tag: tip
974 user: test
975 date: Thu Jan 01 00:00:00 1970 +0000
976 summary: foo -> dir/baz; 1-1+
977
978 diff --git a/foo b/dir/baz
979 copy from foo
980 copy to dir/baz
981 --- a/foo
982 +++ b/dir/baz
983 @@ -2,7 +2,7 @@
984 0
985 0
986 0
987 - 1
988 + 1+
989 2+
990 3+
991 4
992
993
994 $ hg revert -a -C -q
995
871 996 Copies.
872 997
873 998 $ hg copy baz bbaz
874 999 $ sed 's/6/6+/' bbaz > bbaz.new
875 1000 $ mv bbaz.new bbaz
876 1001 $ hg commit -m 'cp baz bbaz; 6-6+'
877 1002 $ hg diff -c .
878 1003 diff --git a/dir/baz b/dir/bbaz
879 1004 copy from dir/baz
880 1005 copy to dir/bbaz
881 1006 --- a/dir/baz
882 1007 +++ b/dir/bbaz
883 1008 @@ -7,7 +7,7 @@
884 1009 3+
885 1010 4
886 1011 5
887 1012 -6
888 1013 +6+
889 1014 7
890 1015 8
891 1016 9
892 1017 $ hg log --copies -f -L bbaz,10:11 -p
893 1018 changeset: 10:91a3d3b6c546
894 1019 tag: tip
895 1020 user: test
896 1021 date: Thu Jan 01 00:00:00 1970 +0000
897 1022 summary: cp baz bbaz; 6-6+
898 1023
899 1024 diff --git a/dir/baz b/dir/bbaz
900 1025 copy from dir/baz
901 1026 copy to dir/bbaz
902 1027 --- a/dir/baz
903 1028 +++ b/dir/bbaz
904 1029 @@ -7,7 +7,7 @@
905 1030 3+
906 1031 4
907 1032 5
908 1033 -6
909 1034 +6+
910 1035 7
911 1036 8
912 1037 9
913 1038
914 1039 changeset: 3:730a61fbaecf
915 1040 user: test
916 1041 date: Thu Jan 01 00:00:00 1970 +0000
917 1042 summary: to 11
918 1043
919 1044 diff --git a/foo b/foo
920 1045 --- a/foo
921 1046 +++ b/foo
922 1047 @@ -6,3 +6,10 @@
923 1048 2+
924 1049 3
925 1050 4
926 1051 +5
927 1052 +6
928 1053 +7
929 1054 +8
930 1055 +9
931 1056 +10
932 1057 +11
933 1058
934 1059 $ hg log -f -L bbaz,10:11 -p
935 1060 changeset: 10:91a3d3b6c546
936 1061 tag: tip
937 1062 user: test
938 1063 date: Thu Jan 01 00:00:00 1970 +0000
939 1064 summary: cp baz bbaz; 6-6+
940 1065
941 1066 diff --git a/dir/baz b/dir/bbaz
942 1067 copy from dir/baz
943 1068 copy to dir/bbaz
944 1069 --- a/dir/baz
945 1070 +++ b/dir/bbaz
946 1071 @@ -7,7 +7,7 @@
947 1072 3+
948 1073 4
949 1074 5
950 1075 -6
951 1076 +6+
952 1077 7
953 1078 8
954 1079 9
955 1080
956 1081 changeset: 3:730a61fbaecf
957 1082 user: test
958 1083 date: Thu Jan 01 00:00:00 1970 +0000
959 1084 summary: to 11
960 1085
961 1086 diff --git a/foo b/foo
962 1087 --- a/foo
963 1088 +++ b/foo
964 1089 @@ -6,3 +6,10 @@
965 1090 2+
966 1091 3
967 1092 4
968 1093 +5
969 1094 +6
970 1095 +7
971 1096 +8
972 1097 +9
973 1098 +10
974 1099 +11
975 1100
976 1101
977 1102 Binary files work but without diff hunks filtering.
978 1103 (Checking w/ and w/o diff.git option.)
979 1104
980 1105 >>> open('binary', 'wb').write(b'this\nis\na\nbinary\0') and None
981 1106 $ hg add binary
982 1107 $ hg ci -m 'add a binary file' --quiet
983 1108 $ hg log -f -L binary,1:2 -p
984 1109 changeset: 11:dc865b608edf
985 1110 tag: tip
986 1111 user: test
987 1112 date: Thu Jan 01 00:00:00 1970 +0000
988 1113 summary: add a binary file
989 1114
990 1115 diff --git a/dir/binary b/dir/binary
991 1116 new file mode 100644
992 1117 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..c2e1fbed209fe919b3f189a6a31950e9adf61e45
993 1118 GIT binary patch
994 1119 literal 17
995 1120 Wc$_QA$SmdpqC~Ew%)G>+N(KNlNClYy
996 1121
997 1122
998 1123 $ hg log -f -L binary,1:2 -p --config diff.git=false
999 1124 changeset: 11:dc865b608edf
1000 1125 tag: tip
1001 1126 user: test
1002 1127 date: Thu Jan 01 00:00:00 1970 +0000
1003 1128 summary: add a binary file
1004 1129
1005 1130 diff -r 91a3d3b6c546 -r dc865b608edf dir/binary
1006 1131 Binary file dir/binary has changed
1007 1132
1008 1133
1009 1134 Option --follow is required.
1010 1135
1011 1136 $ hg log -L foo,5:7
1012 1137 abort: --line-range requires --follow
1013 1138 [255]
1014 1139
1015 1140 Non-exact pattern kinds are not allowed.
1016 1141
1017 1142 $ cd ..
1018 1143 $ hg log -f -L glob:*a*,1:2
1019 1144 hg: parse error: line range pattern 'glob:*a*' must match exactly one file
1020 1145 [255]
1021 1146
1022 1147 We get an error for removed files.
1023 1148
1024 1149 $ hg rm dir/baz
1025 1150 $ hg ci -m 'remove baz' --quiet
1026 1151 $ hg log -f -L dir/baz,5:7 -p
1027 1152 abort: cannot follow file not in parent revision: "dir/baz"
1028 1153 [255]
@@ -1,2088 +1,2088 b''
1 1 test merge-tools configuration - mostly exercising filemerge.py
2 2
3 3 $ unset HGMERGE # make sure HGMERGE doesn't interfere with the test
4 4 $ cat >> $HGRCPATH << EOF
5 5 > [ui]
6 6 > merge=
7 7 > EOF
8 8 $ hg init repo
9 9 $ cd repo
10 10
11 11 revision 0
12 12
13 13 $ echo "revision 0" > f
14 14 $ echo "space" >> f
15 15 $ hg commit -Am "revision 0"
16 16 adding f
17 17
18 18 revision 1
19 19
20 20 $ echo "revision 1" > f
21 21 $ echo "space" >> f
22 22 $ hg commit -Am "revision 1"
23 23 $ hg update 0 > /dev/null
24 24
25 25 revision 2
26 26
27 27 $ echo "revision 2" > f
28 28 $ echo "space" >> f
29 29 $ hg commit -Am "revision 2"
30 30 created new head
31 31 $ hg update 0 > /dev/null
32 32
33 33 revision 3 - simple to merge
34 34
35 35 $ echo "revision 3" >> f
36 36 $ hg commit -Am "revision 3"
37 37 created new head
38 38
39 39 revision 4 - hard to merge
40 40
41 41 $ hg update 0 > /dev/null
42 42 $ echo "revision 4" > f
43 43 $ hg commit -Am "revision 4"
44 44 created new head
45 45
46 46 $ echo "[merge-tools]" > .hg/hgrc
47 47
48 48 $ beforemerge() {
49 49 > cat .hg/hgrc
50 50 > echo "# hg update -C 1"
51 51 > hg update -C 1 > /dev/null
52 52 > }
53 53 $ aftermerge() {
54 54 > echo "# cat f"
55 55 > cat f
56 56 > echo "# hg stat"
57 57 > hg stat
58 58 > echo "# hg resolve --list"
59 59 > hg resolve --list
60 60 > rm -f f.orig
61 61 > }
62 62
63 63 Tool selection
64 64
65 65 default is internal merge:
66 66
67 67 $ beforemerge
68 68 [merge-tools]
69 69 # hg update -C 1
70 70
71 71 hg merge -r 2
72 72 override $PATH to ensure hgmerge not visible; use $PYTHON in case we're
73 73 running from a devel copy, not a temp installation
74 74
75 $ PATH="$BINDIR:/usr/sbin" "$PYTHON" "$BINDIR"/hg merge -r 2
75 $ PATH="/usr/sbin" "$PYTHON" "$BINDIR"/hg merge -r 2
76 76 merging f
77 77 warning: conflicts while merging f! (edit, then use 'hg resolve --mark')
78 78 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
79 79 use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
80 80 [1]
81 81 $ aftermerge
82 82 # cat f
83 83 <<<<<<< working copy: ef83787e2614 - test: revision 1
84 84 revision 1
85 85 =======
86 86 revision 2
87 87 >>>>>>> merge rev: 0185f4e0cf02 - test: revision 2
88 88 space
89 89 # hg stat
90 90 M f
91 91 ? f.orig
92 92 # hg resolve --list
93 93 U f
94 94
95 95 simplest hgrc using false for merge:
96 96
97 97 $ echo "false.whatever=" >> .hg/hgrc
98 98 $ beforemerge
99 99 [merge-tools]
100 100 false.whatever=
101 101 # hg update -C 1
102 102 $ hg merge -r 2
103 103 merging f
104 104 merging f failed!
105 105 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
106 106 use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
107 107 [1]
108 108 $ aftermerge
109 109 # cat f
110 110 revision 1
111 111 space
112 112 # hg stat
113 113 M f
114 114 ? f.orig
115 115 # hg resolve --list
116 116 U f
117 117
118 118 #if unix-permissions
119 119
120 120 unexecutable file in $PATH shouldn't be found:
121 121
122 122 $ echo "echo fail" > false
123 123 $ hg up -qC 1
124 $ PATH="`pwd`:$BINDIR:/usr/sbin" "$PYTHON" "$BINDIR"/hg merge -r 2
124 $ PATH="`pwd`:/usr/sbin" "$PYTHON" "$BINDIR"/hg merge -r 2
125 125 merging f
126 126 warning: conflicts while merging f! (edit, then use 'hg resolve --mark')
127 127 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
128 128 use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
129 129 [1]
130 130 $ rm false
131 131
132 132 #endif
133 133
134 134 executable directory in $PATH shouldn't be found:
135 135
136 136 $ mkdir false
137 137 $ hg up -qC 1
138 $ PATH="`pwd`:$BINDIR:/usr/sbin" "$PYTHON" "$BINDIR"/hg merge -r 2
138 $ PATH="`pwd`:/usr/sbin" "$PYTHON" "$BINDIR"/hg merge -r 2
139 139 merging f
140 140 warning: conflicts while merging f! (edit, then use 'hg resolve --mark')
141 141 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
142 142 use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
143 143 [1]
144 144 $ rmdir false
145 145
146 146 true with higher .priority gets precedence:
147 147
148 148 $ echo "true.priority=1" >> .hg/hgrc
149 149 $ beforemerge
150 150 [merge-tools]
151 151 false.whatever=
152 152 true.priority=1
153 153 # hg update -C 1
154 154 $ hg merge -r 2
155 155 merging f
156 156 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
157 157 (branch merge, don't forget to commit)
158 158 $ aftermerge
159 159 # cat f
160 160 revision 1
161 161 space
162 162 # hg stat
163 163 M f
164 164 # hg resolve --list
165 165 R f
166 166
167 167 unless lowered on command line:
168 168
169 169 $ beforemerge
170 170 [merge-tools]
171 171 false.whatever=
172 172 true.priority=1
173 173 # hg update -C 1
174 174 $ hg merge -r 2 --config merge-tools.true.priority=-7
175 175 merging f
176 176 merging f failed!
177 177 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
178 178 use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
179 179 [1]
180 180 $ aftermerge
181 181 # cat f
182 182 revision 1
183 183 space
184 184 # hg stat
185 185 M f
186 186 ? f.orig
187 187 # hg resolve --list
188 188 U f
189 189
190 190 or false set higher on command line:
191 191
192 192 $ beforemerge
193 193 [merge-tools]
194 194 false.whatever=
195 195 true.priority=1
196 196 # hg update -C 1
197 197 $ hg merge -r 2 --config merge-tools.false.priority=117
198 198 merging f
199 199 merging f failed!
200 200 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
201 201 use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
202 202 [1]
203 203 $ aftermerge
204 204 # cat f
205 205 revision 1
206 206 space
207 207 # hg stat
208 208 M f
209 209 ? f.orig
210 210 # hg resolve --list
211 211 U f
212 212
213 213 or true set to disabled:
214 214 $ beforemerge
215 215 [merge-tools]
216 216 false.whatever=
217 217 true.priority=1
218 218 # hg update -C 1
219 219 $ hg merge -r 2 --config merge-tools.true.disabled=yes
220 220 merging f
221 221 merging f failed!
222 222 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
223 223 use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
224 224 [1]
225 225 $ aftermerge
226 226 # cat f
227 227 revision 1
228 228 space
229 229 # hg stat
230 230 M f
231 231 ? f.orig
232 232 # hg resolve --list
233 233 U f
234 234
235 235 or true.executable not found in PATH:
236 236
237 237 $ beforemerge
238 238 [merge-tools]
239 239 false.whatever=
240 240 true.priority=1
241 241 # hg update -C 1
242 242 $ hg merge -r 2 --config merge-tools.true.executable=nonexistentmergetool
243 243 merging f
244 244 merging f failed!
245 245 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
246 246 use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
247 247 [1]
248 248 $ aftermerge
249 249 # cat f
250 250 revision 1
251 251 space
252 252 # hg stat
253 253 M f
254 254 ? f.orig
255 255 # hg resolve --list
256 256 U f
257 257
258 258 or true.executable with bogus path:
259 259
260 260 $ beforemerge
261 261 [merge-tools]
262 262 false.whatever=
263 263 true.priority=1
264 264 # hg update -C 1
265 265 $ hg merge -r 2 --config merge-tools.true.executable=/nonexistent/mergetool
266 266 merging f
267 267 merging f failed!
268 268 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
269 269 use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
270 270 [1]
271 271 $ aftermerge
272 272 # cat f
273 273 revision 1
274 274 space
275 275 # hg stat
276 276 M f
277 277 ? f.orig
278 278 # hg resolve --list
279 279 U f
280 280
281 281 but true.executable set to cat found in PATH works:
282 282
283 283 $ echo "true.executable=cat" >> .hg/hgrc
284 284 $ beforemerge
285 285 [merge-tools]
286 286 false.whatever=
287 287 true.priority=1
288 288 true.executable=cat
289 289 # hg update -C 1
290 290 $ hg merge -r 2
291 291 merging f
292 292 revision 1
293 293 space
294 294 revision 0
295 295 space
296 296 revision 2
297 297 space
298 298 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
299 299 (branch merge, don't forget to commit)
300 300 $ aftermerge
301 301 # cat f
302 302 revision 1
303 303 space
304 304 # hg stat
305 305 M f
306 306 # hg resolve --list
307 307 R f
308 308
309 309 and true.executable set to cat with path works:
310 310
311 311 $ beforemerge
312 312 [merge-tools]
313 313 false.whatever=
314 314 true.priority=1
315 315 true.executable=cat
316 316 # hg update -C 1
317 317 $ hg merge -r 2 --config merge-tools.true.executable=cat
318 318 merging f
319 319 revision 1
320 320 space
321 321 revision 0
322 322 space
323 323 revision 2
324 324 space
325 325 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
326 326 (branch merge, don't forget to commit)
327 327 $ aftermerge
328 328 # cat f
329 329 revision 1
330 330 space
331 331 # hg stat
332 332 M f
333 333 # hg resolve --list
334 334 R f
335 335
336 336 executable set to python script that succeeds:
337 337
338 338 $ cat > "$TESTTMP/myworkingmerge.py" <<EOF
339 339 > def myworkingmergefn(ui, repo, args, **kwargs):
340 340 > return False
341 341 > EOF
342 342 $ beforemerge
343 343 [merge-tools]
344 344 false.whatever=
345 345 true.priority=1
346 346 true.executable=cat
347 347 # hg update -C 1
348 348 $ hg merge -r 2 --config merge-tools.true.executable="python:$TESTTMP/myworkingmerge.py:myworkingmergefn"
349 349 merging f
350 350 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
351 351 (branch merge, don't forget to commit)
352 352 $ aftermerge
353 353 # cat f
354 354 revision 1
355 355 space
356 356 # hg stat
357 357 M f
358 358 # hg resolve --list
359 359 R f
360 360
361 361 executable set to python script that fails:
362 362
363 363 $ cat > "$TESTTMP/mybrokenmerge.py" <<EOF
364 364 > def mybrokenmergefn(ui, repo, args, **kwargs):
365 365 > ui.write(b"some fail message\n")
366 366 > return True
367 367 > EOF
368 368 $ beforemerge
369 369 [merge-tools]
370 370 false.whatever=
371 371 true.priority=1
372 372 true.executable=cat
373 373 # hg update -C 1
374 374 $ hg merge -r 2 --config merge-tools.true.executable="python:$TESTTMP/mybrokenmerge.py:mybrokenmergefn"
375 375 merging f
376 376 some fail message
377 377 abort: $TESTTMP/mybrokenmerge.py hook failed
378 378 [255]
379 379 $ aftermerge
380 380 # cat f
381 381 revision 1
382 382 space
383 383 # hg stat
384 384 ? f.orig
385 385 # hg resolve --list
386 386 U f
387 387
388 388 executable set to python script that is missing function:
389 389
390 390 $ beforemerge
391 391 [merge-tools]
392 392 false.whatever=
393 393 true.priority=1
394 394 true.executable=cat
395 395 # hg update -C 1
396 396 $ hg merge -r 2 --config merge-tools.true.executable="python:$TESTTMP/myworkingmerge.py:missingFunction"
397 397 merging f
398 398 abort: $TESTTMP/myworkingmerge.py does not have function: missingFunction
399 399 [255]
400 400 $ aftermerge
401 401 # cat f
402 402 revision 1
403 403 space
404 404 # hg stat
405 405 ? f.orig
406 406 # hg resolve --list
407 407 U f
408 408
409 409 executable set to missing python script:
410 410
411 411 $ beforemerge
412 412 [merge-tools]
413 413 false.whatever=
414 414 true.priority=1
415 415 true.executable=cat
416 416 # hg update -C 1
417 417 $ hg merge -r 2 --config merge-tools.true.executable="python:$TESTTMP/missingpythonscript.py:mergefn"
418 418 merging f
419 419 abort: loading python merge script failed: $TESTTMP/missingpythonscript.py
420 420 [255]
421 421 $ aftermerge
422 422 # cat f
423 423 revision 1
424 424 space
425 425 # hg stat
426 426 ? f.orig
427 427 # hg resolve --list
428 428 U f
429 429
430 430 executable set to python script but callable function is missing:
431 431
432 432 $ beforemerge
433 433 [merge-tools]
434 434 false.whatever=
435 435 true.priority=1
436 436 true.executable=cat
437 437 # hg update -C 1
438 438 $ hg merge -r 2 --config merge-tools.true.executable="python:$TESTTMP/myworkingmerge.py"
439 439 abort: invalid 'python:' syntax: python:$TESTTMP/myworkingmerge.py
440 440 [255]
441 441 $ aftermerge
442 442 # cat f
443 443 revision 1
444 444 space
445 445 # hg stat
446 446 # hg resolve --list
447 447 U f
448 448
449 449 executable set to python script but callable function is empty string:
450 450
451 451 $ beforemerge
452 452 [merge-tools]
453 453 false.whatever=
454 454 true.priority=1
455 455 true.executable=cat
456 456 # hg update -C 1
457 457 $ hg merge -r 2 --config merge-tools.true.executable="python:$TESTTMP/myworkingmerge.py:"
458 458 abort: invalid 'python:' syntax: python:$TESTTMP/myworkingmerge.py:
459 459 [255]
460 460 $ aftermerge
461 461 # cat f
462 462 revision 1
463 463 space
464 464 # hg stat
465 465 # hg resolve --list
466 466 U f
467 467
468 468 executable set to python script but callable function is missing and path contains colon:
469 469
470 470 $ beforemerge
471 471 [merge-tools]
472 472 false.whatever=
473 473 true.priority=1
474 474 true.executable=cat
475 475 # hg update -C 1
476 476 $ hg merge -r 2 --config merge-tools.true.executable="python:$TESTTMP/some:dir/myworkingmerge.py"
477 477 abort: invalid 'python:' syntax: python:$TESTTMP/some:dir/myworkingmerge.py
478 478 [255]
479 479 $ aftermerge
480 480 # cat f
481 481 revision 1
482 482 space
483 483 # hg stat
484 484 # hg resolve --list
485 485 U f
486 486
487 487 executable set to python script filename that contains spaces:
488 488
489 489 $ mkdir -p "$TESTTMP/my path"
490 490 $ cat > "$TESTTMP/my path/my working merge with spaces in filename.py" <<EOF
491 491 > def myworkingmergefn(ui, repo, args, **kwargs):
492 492 > return False
493 493 > EOF
494 494 $ beforemerge
495 495 [merge-tools]
496 496 false.whatever=
497 497 true.priority=1
498 498 true.executable=cat
499 499 # hg update -C 1
500 500 $ hg merge -r 2 --config "merge-tools.true.executable=python:$TESTTMP/my path/my working merge with spaces in filename.py:myworkingmergefn"
501 501 merging f
502 502 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
503 503 (branch merge, don't forget to commit)
504 504 $ aftermerge
505 505 # cat f
506 506 revision 1
507 507 space
508 508 # hg stat
509 509 M f
510 510 # hg resolve --list
511 511 R f
512 512
513 513 #if unix-permissions
514 514
515 515 environment variables in true.executable are handled:
516 516
517 517 $ echo 'echo "custom merge tool"' > .hg/merge.sh
518 518 $ beforemerge
519 519 [merge-tools]
520 520 false.whatever=
521 521 true.priority=1
522 522 true.executable=cat
523 523 # hg update -C 1
524 524 $ hg --config merge-tools.true.executable='sh' \
525 525 > --config merge-tools.true.args=.hg/merge.sh \
526 526 > merge -r 2
527 527 merging f
528 528 custom merge tool
529 529 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
530 530 (branch merge, don't forget to commit)
531 531 $ aftermerge
532 532 # cat f
533 533 revision 1
534 534 space
535 535 # hg stat
536 536 M f
537 537 # hg resolve --list
538 538 R f
539 539
540 540 #endif
541 541
542 542 Tool selection and merge-patterns
543 543
544 544 merge-patterns specifies new tool false:
545 545
546 546 $ beforemerge
547 547 [merge-tools]
548 548 false.whatever=
549 549 true.priority=1
550 550 true.executable=cat
551 551 # hg update -C 1
552 552 $ hg merge -r 2 --config merge-patterns.f=false
553 553 merging f
554 554 merging f failed!
555 555 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
556 556 use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
557 557 [1]
558 558 $ aftermerge
559 559 # cat f
560 560 revision 1
561 561 space
562 562 # hg stat
563 563 M f
564 564 ? f.orig
565 565 # hg resolve --list
566 566 U f
567 567
568 568 merge-patterns specifies executable not found in PATH and gets warning:
569 569
570 570 $ beforemerge
571 571 [merge-tools]
572 572 false.whatever=
573 573 true.priority=1
574 574 true.executable=cat
575 575 # hg update -C 1
576 576 $ hg merge -r 2 --config merge-patterns.f=true --config merge-tools.true.executable=nonexistentmergetool
577 577 couldn't find merge tool true (for pattern f)
578 578 merging f
579 579 couldn't find merge tool true (for pattern f)
580 580 merging f failed!
581 581 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
582 582 use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
583 583 [1]
584 584 $ aftermerge
585 585 # cat f
586 586 revision 1
587 587 space
588 588 # hg stat
589 589 M f
590 590 ? f.orig
591 591 # hg resolve --list
592 592 U f
593 593
594 594 merge-patterns specifies executable with bogus path and gets warning:
595 595
596 596 $ beforemerge
597 597 [merge-tools]
598 598 false.whatever=
599 599 true.priority=1
600 600 true.executable=cat
601 601 # hg update -C 1
602 602 $ hg merge -r 2 --config merge-patterns.f=true --config merge-tools.true.executable=/nonexistent/mergetool
603 603 couldn't find merge tool true (for pattern f)
604 604 merging f
605 605 couldn't find merge tool true (for pattern f)
606 606 merging f failed!
607 607 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
608 608 use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
609 609 [1]
610 610 $ aftermerge
611 611 # cat f
612 612 revision 1
613 613 space
614 614 # hg stat
615 615 M f
616 616 ? f.orig
617 617 # hg resolve --list
618 618 U f
619 619
620 620 ui.merge overrules priority
621 621
622 622 ui.merge specifies false:
623 623
624 624 $ beforemerge
625 625 [merge-tools]
626 626 false.whatever=
627 627 true.priority=1
628 628 true.executable=cat
629 629 # hg update -C 1
630 630 $ hg merge -r 2 --config ui.merge=false
631 631 merging f
632 632 merging f failed!
633 633 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
634 634 use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
635 635 [1]
636 636 $ aftermerge
637 637 # cat f
638 638 revision 1
639 639 space
640 640 # hg stat
641 641 M f
642 642 ? f.orig
643 643 # hg resolve --list
644 644 U f
645 645
646 646 ui.merge specifies internal:fail:
647 647
648 648 $ beforemerge
649 649 [merge-tools]
650 650 false.whatever=
651 651 true.priority=1
652 652 true.executable=cat
653 653 # hg update -C 1
654 654 $ hg merge -r 2 --config ui.merge=internal:fail
655 655 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
656 656 use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
657 657 [1]
658 658 $ aftermerge
659 659 # cat f
660 660 revision 1
661 661 space
662 662 # hg stat
663 663 M f
664 664 # hg resolve --list
665 665 U f
666 666
667 667 ui.merge specifies :local (without internal prefix):
668 668
669 669 $ beforemerge
670 670 [merge-tools]
671 671 false.whatever=
672 672 true.priority=1
673 673 true.executable=cat
674 674 # hg update -C 1
675 675 $ hg merge -r 2 --config ui.merge=:local
676 676 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
677 677 (branch merge, don't forget to commit)
678 678 $ aftermerge
679 679 # cat f
680 680 revision 1
681 681 space
682 682 # hg stat
683 683 M f
684 684 # hg resolve --list
685 685 R f
686 686
687 687 ui.merge specifies internal:other:
688 688
689 689 $ beforemerge
690 690 [merge-tools]
691 691 false.whatever=
692 692 true.priority=1
693 693 true.executable=cat
694 694 # hg update -C 1
695 695 $ hg merge -r 2 --config ui.merge=internal:other
696 696 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
697 697 (branch merge, don't forget to commit)
698 698 $ aftermerge
699 699 # cat f
700 700 revision 2
701 701 space
702 702 # hg stat
703 703 M f
704 704 # hg resolve --list
705 705 R f
706 706
707 707 ui.merge specifies internal:prompt:
708 708
709 709 $ beforemerge
710 710 [merge-tools]
711 711 false.whatever=
712 712 true.priority=1
713 713 true.executable=cat
714 714 # hg update -C 1
715 715 $ hg merge -r 2 --config ui.merge=internal:prompt
716 716 file 'f' needs to be resolved.
717 717 You can keep (l)ocal [working copy], take (o)ther [merge rev], or leave (u)nresolved.
718 718 What do you want to do? u
719 719 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
720 720 use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
721 721 [1]
722 722 $ aftermerge
723 723 # cat f
724 724 revision 1
725 725 space
726 726 # hg stat
727 727 M f
728 728 # hg resolve --list
729 729 U f
730 730
731 731 ui.merge specifies :prompt, with 'leave unresolved' chosen
732 732
733 733 $ beforemerge
734 734 [merge-tools]
735 735 false.whatever=
736 736 true.priority=1
737 737 true.executable=cat
738 738 # hg update -C 1
739 739 $ hg merge -r 2 --config ui.merge=:prompt --config ui.interactive=True << EOF
740 740 > u
741 741 > EOF
742 742 file 'f' needs to be resolved.
743 743 You can keep (l)ocal [working copy], take (o)ther [merge rev], or leave (u)nresolved.
744 744 What do you want to do? u
745 745 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
746 746 use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
747 747 [1]
748 748 $ aftermerge
749 749 # cat f
750 750 revision 1
751 751 space
752 752 # hg stat
753 753 M f
754 754 # hg resolve --list
755 755 U f
756 756
757 757 prompt with EOF
758 758
759 759 $ beforemerge
760 760 [merge-tools]
761 761 false.whatever=
762 762 true.priority=1
763 763 true.executable=cat
764 764 # hg update -C 1
765 765 $ hg merge -r 2 --config ui.merge=internal:prompt --config ui.interactive=true
766 766 file 'f' needs to be resolved.
767 767 You can keep (l)ocal [working copy], take (o)ther [merge rev], or leave (u)nresolved.
768 768 What do you want to do?
769 769 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
770 770 use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
771 771 [1]
772 772 $ aftermerge
773 773 # cat f
774 774 revision 1
775 775 space
776 776 # hg stat
777 777 M f
778 778 # hg resolve --list
779 779 U f
780 780 $ hg resolve --all --config ui.merge=internal:prompt --config ui.interactive=true
781 781 file 'f' needs to be resolved.
782 782 You can keep (l)ocal [working copy], take (o)ther [merge rev], or leave (u)nresolved.
783 783 What do you want to do?
784 784 [1]
785 785 $ aftermerge
786 786 # cat f
787 787 revision 1
788 788 space
789 789 # hg stat
790 790 M f
791 791 ? f.orig
792 792 # hg resolve --list
793 793 U f
794 794 $ rm f
795 795 $ hg resolve --all --config ui.merge=internal:prompt --config ui.interactive=true
796 796 file 'f' needs to be resolved.
797 797 You can keep (l)ocal [working copy], take (o)ther [merge rev], or leave (u)nresolved.
798 798 What do you want to do?
799 799 [1]
800 800 $ aftermerge
801 801 # cat f
802 802 revision 1
803 803 space
804 804 # hg stat
805 805 M f
806 806 # hg resolve --list
807 807 U f
808 808 $ hg resolve --all --config ui.merge=internal:prompt
809 809 file 'f' needs to be resolved.
810 810 You can keep (l)ocal [working copy], take (o)ther [merge rev], or leave (u)nresolved.
811 811 What do you want to do? u
812 812 [1]
813 813 $ aftermerge
814 814 # cat f
815 815 revision 1
816 816 space
817 817 # hg stat
818 818 M f
819 819 ? f.orig
820 820 # hg resolve --list
821 821 U f
822 822
823 823 ui.merge specifies internal:dump:
824 824
825 825 $ beforemerge
826 826 [merge-tools]
827 827 false.whatever=
828 828 true.priority=1
829 829 true.executable=cat
830 830 # hg update -C 1
831 831 $ hg merge -r 2 --config ui.merge=internal:dump
832 832 merging f
833 833 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
834 834 use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
835 835 [1]
836 836 $ aftermerge
837 837 # cat f
838 838 revision 1
839 839 space
840 840 # hg stat
841 841 M f
842 842 ? f.base
843 843 ? f.local
844 844 ? f.orig
845 845 ? f.other
846 846 # hg resolve --list
847 847 U f
848 848
849 849 f.base:
850 850
851 851 $ cat f.base
852 852 revision 0
853 853 space
854 854
855 855 f.local:
856 856
857 857 $ cat f.local
858 858 revision 1
859 859 space
860 860
861 861 f.other:
862 862
863 863 $ cat f.other
864 864 revision 2
865 865 space
866 866 $ rm f.base f.local f.other
867 867
868 868 check that internal:dump doesn't dump files if premerge runs
869 869 successfully
870 870
871 871 $ beforemerge
872 872 [merge-tools]
873 873 false.whatever=
874 874 true.priority=1
875 875 true.executable=cat
876 876 # hg update -C 1
877 877 $ hg merge -r 3 --config ui.merge=internal:dump
878 878 merging f
879 879 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
880 880 (branch merge, don't forget to commit)
881 881
882 882 $ aftermerge
883 883 # cat f
884 884 revision 1
885 885 space
886 886 revision 3
887 887 # hg stat
888 888 M f
889 889 # hg resolve --list
890 890 R f
891 891
892 892 check that internal:forcedump dumps files, even if local and other can
893 893 be merged easily
894 894
895 895 $ beforemerge
896 896 [merge-tools]
897 897 false.whatever=
898 898 true.priority=1
899 899 true.executable=cat
900 900 # hg update -C 1
901 901 $ hg merge -r 3 --config ui.merge=internal:forcedump
902 902 merging f
903 903 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
904 904 use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
905 905 [1]
906 906 $ aftermerge
907 907 # cat f
908 908 revision 1
909 909 space
910 910 # hg stat
911 911 M f
912 912 ? f.base
913 913 ? f.local
914 914 ? f.orig
915 915 ? f.other
916 916 # hg resolve --list
917 917 U f
918 918
919 919 $ cat f.base
920 920 revision 0
921 921 space
922 922
923 923 $ cat f.local
924 924 revision 1
925 925 space
926 926
927 927 $ cat f.other
928 928 revision 0
929 929 space
930 930 revision 3
931 931
932 932 $ rm -f f.base f.local f.other
933 933
934 934 ui.merge specifies internal:other but is overruled by pattern for false:
935 935
936 936 $ beforemerge
937 937 [merge-tools]
938 938 false.whatever=
939 939 true.priority=1
940 940 true.executable=cat
941 941 # hg update -C 1
942 942 $ hg merge -r 2 --config ui.merge=internal:other --config merge-patterns.f=false
943 943 merging f
944 944 merging f failed!
945 945 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
946 946 use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
947 947 [1]
948 948 $ aftermerge
949 949 # cat f
950 950 revision 1
951 951 space
952 952 # hg stat
953 953 M f
954 954 ? f.orig
955 955 # hg resolve --list
956 956 U f
957 957
958 958 Premerge
959 959
960 960 ui.merge specifies internal:other but is overruled by --tool=false
961 961
962 962 $ beforemerge
963 963 [merge-tools]
964 964 false.whatever=
965 965 true.priority=1
966 966 true.executable=cat
967 967 # hg update -C 1
968 968 $ hg merge -r 2 --config ui.merge=internal:other --tool=false
969 969 merging f
970 970 merging f failed!
971 971 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
972 972 use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
973 973 [1]
974 974 $ aftermerge
975 975 # cat f
976 976 revision 1
977 977 space
978 978 # hg stat
979 979 M f
980 980 ? f.orig
981 981 # hg resolve --list
982 982 U f
983 983
984 984 HGMERGE specifies internal:other but is overruled by --tool=false
985 985
986 986 $ HGMERGE=internal:other ; export HGMERGE
987 987 $ beforemerge
988 988 [merge-tools]
989 989 false.whatever=
990 990 true.priority=1
991 991 true.executable=cat
992 992 # hg update -C 1
993 993 $ hg merge -r 2 --tool=false
994 994 merging f
995 995 merging f failed!
996 996 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
997 997 use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
998 998 [1]
999 999 $ aftermerge
1000 1000 # cat f
1001 1001 revision 1
1002 1002 space
1003 1003 # hg stat
1004 1004 M f
1005 1005 ? f.orig
1006 1006 # hg resolve --list
1007 1007 U f
1008 1008
1009 1009 $ unset HGMERGE # make sure HGMERGE doesn't interfere with remaining tests
1010 1010
1011 1011 update is a merge ...
1012 1012
1013 1013 (this also tests that files reverted with '--rev REV' are treated as
1014 1014 "modified", even if none of mode, size and timestamp of them isn't
1015 1015 changed on the filesystem (see also issue4583))
1016 1016
1017 1017 $ cat >> $HGRCPATH <<EOF
1018 1018 > [fakedirstatewritetime]
1019 1019 > # emulate invoking dirstate.write() via repo.status()
1020 1020 > # at 2000-01-01 00:00
1021 1021 > fakenow = 200001010000
1022 1022 > EOF
1023 1023
1024 1024 $ beforemerge
1025 1025 [merge-tools]
1026 1026 false.whatever=
1027 1027 true.priority=1
1028 1028 true.executable=cat
1029 1029 # hg update -C 1
1030 1030 $ hg update -q 0
1031 1031 $ f -s f
1032 1032 f: size=17
1033 1033 $ touch -t 200001010000 f
1034 1034 $ hg debugrebuildstate
1035 1035 $ cat >> $HGRCPATH <<EOF
1036 1036 > [extensions]
1037 1037 > fakedirstatewritetime = $TESTDIR/fakedirstatewritetime.py
1038 1038 > EOF
1039 1039 $ hg revert -q -r 1 .
1040 1040 $ cat >> $HGRCPATH <<EOF
1041 1041 > [extensions]
1042 1042 > fakedirstatewritetime = !
1043 1043 > EOF
1044 1044 $ f -s f
1045 1045 f: size=17
1046 1046 $ touch -t 200001010000 f
1047 1047 $ hg status f
1048 1048 M f
1049 1049 $ hg update -r 2
1050 1050 merging f
1051 1051 revision 1
1052 1052 space
1053 1053 revision 0
1054 1054 space
1055 1055 revision 2
1056 1056 space
1057 1057 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
1058 1058 $ aftermerge
1059 1059 # cat f
1060 1060 revision 1
1061 1061 space
1062 1062 # hg stat
1063 1063 M f
1064 1064 # hg resolve --list
1065 1065 R f
1066 1066
1067 1067 update should also have --tool
1068 1068
1069 1069 $ beforemerge
1070 1070 [merge-tools]
1071 1071 false.whatever=
1072 1072 true.priority=1
1073 1073 true.executable=cat
1074 1074 # hg update -C 1
1075 1075 $ hg update -q 0
1076 1076 $ f -s f
1077 1077 f: size=17
1078 1078 $ touch -t 200001010000 f
1079 1079 $ hg debugrebuildstate
1080 1080 $ cat >> $HGRCPATH <<EOF
1081 1081 > [extensions]
1082 1082 > fakedirstatewritetime = $TESTDIR/fakedirstatewritetime.py
1083 1083 > EOF
1084 1084 $ hg revert -q -r 1 .
1085 1085 $ cat >> $HGRCPATH <<EOF
1086 1086 > [extensions]
1087 1087 > fakedirstatewritetime = !
1088 1088 > EOF
1089 1089 $ f -s f
1090 1090 f: size=17
1091 1091 $ touch -t 200001010000 f
1092 1092 $ hg status f
1093 1093 M f
1094 1094 $ hg update -r 2 --tool false
1095 1095 merging f
1096 1096 merging f failed!
1097 1097 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
1098 1098 use 'hg resolve' to retry unresolved file merges
1099 1099 [1]
1100 1100 $ aftermerge
1101 1101 # cat f
1102 1102 revision 1
1103 1103 space
1104 1104 # hg stat
1105 1105 M f
1106 1106 ? f.orig
1107 1107 # hg resolve --list
1108 1108 U f
1109 1109
1110 1110 Default is silent simplemerge:
1111 1111
1112 1112 $ beforemerge
1113 1113 [merge-tools]
1114 1114 false.whatever=
1115 1115 true.priority=1
1116 1116 true.executable=cat
1117 1117 # hg update -C 1
1118 1118 $ hg merge -r 3
1119 1119 merging f
1120 1120 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
1121 1121 (branch merge, don't forget to commit)
1122 1122 $ aftermerge
1123 1123 # cat f
1124 1124 revision 1
1125 1125 space
1126 1126 revision 3
1127 1127 # hg stat
1128 1128 M f
1129 1129 # hg resolve --list
1130 1130 R f
1131 1131
1132 1132 .premerge=True is same:
1133 1133
1134 1134 $ beforemerge
1135 1135 [merge-tools]
1136 1136 false.whatever=
1137 1137 true.priority=1
1138 1138 true.executable=cat
1139 1139 # hg update -C 1
1140 1140 $ hg merge -r 3 --config merge-tools.true.premerge=True
1141 1141 merging f
1142 1142 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
1143 1143 (branch merge, don't forget to commit)
1144 1144 $ aftermerge
1145 1145 # cat f
1146 1146 revision 1
1147 1147 space
1148 1148 revision 3
1149 1149 # hg stat
1150 1150 M f
1151 1151 # hg resolve --list
1152 1152 R f
1153 1153
1154 1154 .premerge=False executes merge-tool:
1155 1155
1156 1156 $ beforemerge
1157 1157 [merge-tools]
1158 1158 false.whatever=
1159 1159 true.priority=1
1160 1160 true.executable=cat
1161 1161 # hg update -C 1
1162 1162 $ hg merge -r 3 --config merge-tools.true.premerge=False
1163 1163 merging f
1164 1164 revision 1
1165 1165 space
1166 1166 revision 0
1167 1167 space
1168 1168 revision 0
1169 1169 space
1170 1170 revision 3
1171 1171 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
1172 1172 (branch merge, don't forget to commit)
1173 1173 $ aftermerge
1174 1174 # cat f
1175 1175 revision 1
1176 1176 space
1177 1177 # hg stat
1178 1178 M f
1179 1179 # hg resolve --list
1180 1180 R f
1181 1181
1182 1182 premerge=keep keeps conflict markers in:
1183 1183
1184 1184 $ beforemerge
1185 1185 [merge-tools]
1186 1186 false.whatever=
1187 1187 true.priority=1
1188 1188 true.executable=cat
1189 1189 # hg update -C 1
1190 1190 $ hg merge -r 4 --config merge-tools.true.premerge=keep
1191 1191 merging f
1192 1192 <<<<<<< working copy: ef83787e2614 - test: revision 1
1193 1193 revision 1
1194 1194 space
1195 1195 =======
1196 1196 revision 4
1197 1197 >>>>>>> merge rev: 81448d39c9a0 - test: revision 4
1198 1198 revision 0
1199 1199 space
1200 1200 revision 4
1201 1201 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
1202 1202 (branch merge, don't forget to commit)
1203 1203 $ aftermerge
1204 1204 # cat f
1205 1205 <<<<<<< working copy: ef83787e2614 - test: revision 1
1206 1206 revision 1
1207 1207 space
1208 1208 =======
1209 1209 revision 4
1210 1210 >>>>>>> merge rev: 81448d39c9a0 - test: revision 4
1211 1211 # hg stat
1212 1212 M f
1213 1213 # hg resolve --list
1214 1214 R f
1215 1215
1216 1216 premerge=keep-merge3 keeps conflict markers with base content:
1217 1217
1218 1218 $ beforemerge
1219 1219 [merge-tools]
1220 1220 false.whatever=
1221 1221 true.priority=1
1222 1222 true.executable=cat
1223 1223 # hg update -C 1
1224 1224 $ hg merge -r 4 --config merge-tools.true.premerge=keep-merge3
1225 1225 merging f
1226 1226 <<<<<<< working copy: ef83787e2614 - test: revision 1
1227 1227 revision 1
1228 1228 space
1229 1229 ||||||| base
1230 1230 revision 0
1231 1231 space
1232 1232 =======
1233 1233 revision 4
1234 1234 >>>>>>> merge rev: 81448d39c9a0 - test: revision 4
1235 1235 revision 0
1236 1236 space
1237 1237 revision 4
1238 1238 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
1239 1239 (branch merge, don't forget to commit)
1240 1240 $ aftermerge
1241 1241 # cat f
1242 1242 <<<<<<< working copy: ef83787e2614 - test: revision 1
1243 1243 revision 1
1244 1244 space
1245 1245 ||||||| base
1246 1246 revision 0
1247 1247 space
1248 1248 =======
1249 1249 revision 4
1250 1250 >>>>>>> merge rev: 81448d39c9a0 - test: revision 4
1251 1251 # hg stat
1252 1252 M f
1253 1253 # hg resolve --list
1254 1254 R f
1255 1255
1256 1256 premerge=keep respects ui.mergemarkers=basic:
1257 1257
1258 1258 $ beforemerge
1259 1259 [merge-tools]
1260 1260 false.whatever=
1261 1261 true.priority=1
1262 1262 true.executable=cat
1263 1263 # hg update -C 1
1264 1264 $ hg merge -r 4 --config merge-tools.true.premerge=keep --config ui.mergemarkers=basic
1265 1265 merging f
1266 1266 <<<<<<< working copy
1267 1267 revision 1
1268 1268 space
1269 1269 =======
1270 1270 revision 4
1271 1271 >>>>>>> merge rev
1272 1272 revision 0
1273 1273 space
1274 1274 revision 4
1275 1275 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
1276 1276 (branch merge, don't forget to commit)
1277 1277 $ aftermerge
1278 1278 # cat f
1279 1279 <<<<<<< working copy
1280 1280 revision 1
1281 1281 space
1282 1282 =======
1283 1283 revision 4
1284 1284 >>>>>>> merge rev
1285 1285 # hg stat
1286 1286 M f
1287 1287 # hg resolve --list
1288 1288 R f
1289 1289
1290 1290 premerge=keep ignores ui.mergemarkers=basic if true.mergemarkers=detailed:
1291 1291
1292 1292 $ beforemerge
1293 1293 [merge-tools]
1294 1294 false.whatever=
1295 1295 true.priority=1
1296 1296 true.executable=cat
1297 1297 # hg update -C 1
1298 1298 $ hg merge -r 4 --config merge-tools.true.premerge=keep \
1299 1299 > --config ui.mergemarkers=basic \
1300 1300 > --config merge-tools.true.mergemarkers=detailed
1301 1301 merging f
1302 1302 <<<<<<< working copy: ef83787e2614 - test: revision 1
1303 1303 revision 1
1304 1304 space
1305 1305 =======
1306 1306 revision 4
1307 1307 >>>>>>> merge rev: 81448d39c9a0 - test: revision 4
1308 1308 revision 0
1309 1309 space
1310 1310 revision 4
1311 1311 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
1312 1312 (branch merge, don't forget to commit)
1313 1313 $ aftermerge
1314 1314 # cat f
1315 1315 <<<<<<< working copy: ef83787e2614 - test: revision 1
1316 1316 revision 1
1317 1317 space
1318 1318 =======
1319 1319 revision 4
1320 1320 >>>>>>> merge rev: 81448d39c9a0 - test: revision 4
1321 1321 # hg stat
1322 1322 M f
1323 1323 # hg resolve --list
1324 1324 R f
1325 1325
1326 1326 premerge=keep respects ui.mergemarkertemplate instead of
1327 1327 true.mergemarkertemplate if true.mergemarkers=basic:
1328 1328
1329 1329 $ beforemerge
1330 1330 [merge-tools]
1331 1331 false.whatever=
1332 1332 true.priority=1
1333 1333 true.executable=cat
1334 1334 # hg update -C 1
1335 1335 $ hg merge -r 4 --config merge-tools.true.premerge=keep \
1336 1336 > --config ui.mergemarkertemplate='uitmpl {rev}' \
1337 1337 > --config merge-tools.true.mergemarkertemplate='tooltmpl {short(node)}'
1338 1338 merging f
1339 1339 <<<<<<< working copy: uitmpl 1
1340 1340 revision 1
1341 1341 space
1342 1342 =======
1343 1343 revision 4
1344 1344 >>>>>>> merge rev: uitmpl 4
1345 1345 revision 0
1346 1346 space
1347 1347 revision 4
1348 1348 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
1349 1349 (branch merge, don't forget to commit)
1350 1350 $ aftermerge
1351 1351 # cat f
1352 1352 <<<<<<< working copy: uitmpl 1
1353 1353 revision 1
1354 1354 space
1355 1355 =======
1356 1356 revision 4
1357 1357 >>>>>>> merge rev: uitmpl 4
1358 1358 # hg stat
1359 1359 M f
1360 1360 # hg resolve --list
1361 1361 R f
1362 1362
1363 1363 premerge=keep respects true.mergemarkertemplate instead of
1364 1364 true.mergemarkertemplate if true.mergemarkers=detailed:
1365 1365
1366 1366 $ beforemerge
1367 1367 [merge-tools]
1368 1368 false.whatever=
1369 1369 true.priority=1
1370 1370 true.executable=cat
1371 1371 # hg update -C 1
1372 1372 $ hg merge -r 4 --config merge-tools.true.premerge=keep \
1373 1373 > --config ui.mergemarkertemplate='uitmpl {rev}' \
1374 1374 > --config merge-tools.true.mergemarkertemplate='tooltmpl {short(node)}' \
1375 1375 > --config merge-tools.true.mergemarkers=detailed
1376 1376 merging f
1377 1377 <<<<<<< working copy: tooltmpl ef83787e2614
1378 1378 revision 1
1379 1379 space
1380 1380 =======
1381 1381 revision 4
1382 1382 >>>>>>> merge rev: tooltmpl 81448d39c9a0
1383 1383 revision 0
1384 1384 space
1385 1385 revision 4
1386 1386 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
1387 1387 (branch merge, don't forget to commit)
1388 1388 $ aftermerge
1389 1389 # cat f
1390 1390 <<<<<<< working copy: tooltmpl ef83787e2614
1391 1391 revision 1
1392 1392 space
1393 1393 =======
1394 1394 revision 4
1395 1395 >>>>>>> merge rev: tooltmpl 81448d39c9a0
1396 1396 # hg stat
1397 1397 M f
1398 1398 # hg resolve --list
1399 1399 R f
1400 1400
1401 1401 Tool execution
1402 1402
1403 1403 set tools.args explicit to include $base $local $other $output:
1404 1404
1405 1405 $ beforemerge
1406 1406 [merge-tools]
1407 1407 false.whatever=
1408 1408 true.priority=1
1409 1409 true.executable=cat
1410 1410 # hg update -C 1
1411 1411 $ hg merge -r 2 --config merge-tools.true.executable=head --config merge-tools.true.args='$base $local $other $output' \
1412 1412 > | sed 's,==> .* <==,==> ... <==,g'
1413 1413 merging f
1414 1414 ==> ... <==
1415 1415 revision 0
1416 1416 space
1417 1417
1418 1418 ==> ... <==
1419 1419 revision 1
1420 1420 space
1421 1421
1422 1422 ==> ... <==
1423 1423 revision 2
1424 1424 space
1425 1425
1426 1426 ==> ... <==
1427 1427 revision 1
1428 1428 space
1429 1429 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
1430 1430 (branch merge, don't forget to commit)
1431 1431 $ aftermerge
1432 1432 # cat f
1433 1433 revision 1
1434 1434 space
1435 1435 # hg stat
1436 1436 M f
1437 1437 # hg resolve --list
1438 1438 R f
1439 1439
1440 1440 Merge with "echo mergeresult > $local":
1441 1441
1442 1442 $ beforemerge
1443 1443 [merge-tools]
1444 1444 false.whatever=
1445 1445 true.priority=1
1446 1446 true.executable=cat
1447 1447 # hg update -C 1
1448 1448 $ hg merge -r 2 --config merge-tools.true.executable=echo --config merge-tools.true.args='mergeresult > $local'
1449 1449 merging f
1450 1450 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
1451 1451 (branch merge, don't forget to commit)
1452 1452 $ aftermerge
1453 1453 # cat f
1454 1454 mergeresult
1455 1455 # hg stat
1456 1456 M f
1457 1457 # hg resolve --list
1458 1458 R f
1459 1459
1460 1460 - and $local is the file f:
1461 1461
1462 1462 $ beforemerge
1463 1463 [merge-tools]
1464 1464 false.whatever=
1465 1465 true.priority=1
1466 1466 true.executable=cat
1467 1467 # hg update -C 1
1468 1468 $ hg merge -r 2 --config merge-tools.true.executable=echo --config merge-tools.true.args='mergeresult > f'
1469 1469 merging f
1470 1470 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
1471 1471 (branch merge, don't forget to commit)
1472 1472 $ aftermerge
1473 1473 # cat f
1474 1474 mergeresult
1475 1475 # hg stat
1476 1476 M f
1477 1477 # hg resolve --list
1478 1478 R f
1479 1479
1480 1480 Merge with "echo mergeresult > $output" - the variable is a bit magic:
1481 1481
1482 1482 $ beforemerge
1483 1483 [merge-tools]
1484 1484 false.whatever=
1485 1485 true.priority=1
1486 1486 true.executable=cat
1487 1487 # hg update -C 1
1488 1488 $ hg merge -r 2 --config merge-tools.true.executable=echo --config merge-tools.true.args='mergeresult > $output'
1489 1489 merging f
1490 1490 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
1491 1491 (branch merge, don't forget to commit)
1492 1492 $ aftermerge
1493 1493 # cat f
1494 1494 mergeresult
1495 1495 # hg stat
1496 1496 M f
1497 1497 # hg resolve --list
1498 1498 R f
1499 1499
1500 1500 Merge using tool with a path that must be quoted:
1501 1501
1502 1502 $ beforemerge
1503 1503 [merge-tools]
1504 1504 false.whatever=
1505 1505 true.priority=1
1506 1506 true.executable=cat
1507 1507 # hg update -C 1
1508 1508 $ cat <<EOF > 'my merge tool'
1509 1509 > cat "\$1" "\$2" "\$3" > "\$4"
1510 1510 > EOF
1511 1511 $ hg --config merge-tools.true.executable='sh' \
1512 1512 > --config merge-tools.true.args='"./my merge tool" $base $local $other $output' \
1513 1513 > merge -r 2
1514 1514 merging f
1515 1515 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
1516 1516 (branch merge, don't forget to commit)
1517 1517 $ rm -f 'my merge tool'
1518 1518 $ aftermerge
1519 1519 # cat f
1520 1520 revision 0
1521 1521 space
1522 1522 revision 1
1523 1523 space
1524 1524 revision 2
1525 1525 space
1526 1526 # hg stat
1527 1527 M f
1528 1528 # hg resolve --list
1529 1529 R f
1530 1530
1531 1531 Merge using a tool that supports labellocal, labelother, and labelbase, checking
1532 1532 that they're quoted properly as well. This is using the default 'basic'
1533 1533 mergemarkers even though ui.mergemarkers is 'detailed', so it's ignoring both
1534 1534 mergemarkertemplate settings:
1535 1535
1536 1536 $ beforemerge
1537 1537 [merge-tools]
1538 1538 false.whatever=
1539 1539 true.priority=1
1540 1540 true.executable=cat
1541 1541 # hg update -C 1
1542 1542 $ cat <<EOF > printargs_merge_tool
1543 1543 > while test \$# -gt 0; do echo arg: \"\$1\"; shift; done
1544 1544 > EOF
1545 1545 $ hg --config merge-tools.true.executable='sh' \
1546 1546 > --config merge-tools.true.args='./printargs_merge_tool ll:$labellocal lo: $labelother lb:$labelbase": "$base' \
1547 1547 > --config merge-tools.true.mergemarkertemplate='tooltmpl {short(node)}' \
1548 1548 > --config ui.mergemarkertemplate='uitmpl {rev}' \
1549 1549 > --config ui.mergemarkers=detailed \
1550 1550 > merge -r 2
1551 1551 merging f
1552 1552 arg: "ll:working copy"
1553 1553 arg: "lo:"
1554 1554 arg: "merge rev"
1555 1555 arg: "lb:base: */f~base.*" (glob)
1556 1556 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
1557 1557 (branch merge, don't forget to commit)
1558 1558 $ rm -f 'printargs_merge_tool'
1559 1559
1560 1560 Same test with experimental.mergetempdirprefix set:
1561 1561
1562 1562 $ beforemerge
1563 1563 [merge-tools]
1564 1564 false.whatever=
1565 1565 true.priority=1
1566 1566 true.executable=cat
1567 1567 # hg update -C 1
1568 1568 $ cat <<EOF > printargs_merge_tool
1569 1569 > while test \$# -gt 0; do echo arg: \"\$1\"; shift; done
1570 1570 > EOF
1571 1571 $ hg --config experimental.mergetempdirprefix=$TESTTMP/hgmerge. \
1572 1572 > --config merge-tools.true.executable='sh' \
1573 1573 > --config merge-tools.true.args='./printargs_merge_tool ll:$labellocal lo: $labelother lb:$labelbase": "$base' \
1574 1574 > --config merge-tools.true.mergemarkertemplate='tooltmpl {short(node)}' \
1575 1575 > --config ui.mergemarkertemplate='uitmpl {rev}' \
1576 1576 > --config ui.mergemarkers=detailed \
1577 1577 > merge -r 2
1578 1578 merging f
1579 1579 arg: "ll:working copy"
1580 1580 arg: "lo:"
1581 1581 arg: "merge rev"
1582 1582 arg: "lb:base: */hgmerge.*/f~base" (glob)
1583 1583 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
1584 1584 (branch merge, don't forget to commit)
1585 1585 $ rm -f 'printargs_merge_tool'
1586 1586
1587 1587 Merge using a tool that supports labellocal, labelother, and labelbase, checking
1588 1588 that they're quoted properly as well. This is using 'detailed' mergemarkers,
1589 1589 even though ui.mergemarkers is 'basic', and using the tool's
1590 1590 mergemarkertemplate:
1591 1591
1592 1592 $ beforemerge
1593 1593 [merge-tools]
1594 1594 false.whatever=
1595 1595 true.priority=1
1596 1596 true.executable=cat
1597 1597 # hg update -C 1
1598 1598 $ cat <<EOF > printargs_merge_tool
1599 1599 > while test \$# -gt 0; do echo arg: \"\$1\"; shift; done
1600 1600 > EOF
1601 1601 $ hg --config merge-tools.true.executable='sh' \
1602 1602 > --config merge-tools.true.args='./printargs_merge_tool ll:$labellocal lo: $labelother lb:$labelbase": "$base' \
1603 1603 > --config merge-tools.true.mergemarkers=detailed \
1604 1604 > --config merge-tools.true.mergemarkertemplate='tooltmpl {short(node)}' \
1605 1605 > --config ui.mergemarkertemplate='uitmpl {rev}' \
1606 1606 > --config ui.mergemarkers=basic \
1607 1607 > merge -r 2
1608 1608 merging f
1609 1609 arg: "ll:working copy: tooltmpl ef83787e2614"
1610 1610 arg: "lo:"
1611 1611 arg: "merge rev: tooltmpl 0185f4e0cf02"
1612 1612 arg: "lb:base: */f~base.*" (glob)
1613 1613 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
1614 1614 (branch merge, don't forget to commit)
1615 1615 $ rm -f 'printargs_merge_tool'
1616 1616
1617 1617 The merge tool still gets labellocal and labelother as 'basic' even when
1618 1618 premerge=keep is used and has 'detailed' markers:
1619 1619
1620 1620 $ beforemerge
1621 1621 [merge-tools]
1622 1622 false.whatever=
1623 1623 true.priority=1
1624 1624 true.executable=cat
1625 1625 # hg update -C 1
1626 1626 $ cat <<EOF > mytool
1627 1627 > echo labellocal: \"\$1\"
1628 1628 > echo labelother: \"\$2\"
1629 1629 > echo "output (arg)": \"\$3\"
1630 1630 > echo "output (contents)":
1631 1631 > cat "\$3"
1632 1632 > EOF
1633 1633 $ hg --config merge-tools.true.executable='sh' \
1634 1634 > --config merge-tools.true.args='mytool $labellocal $labelother $output' \
1635 1635 > --config merge-tools.true.premerge=keep \
1636 1636 > --config merge-tools.true.mergemarkertemplate='tooltmpl {short(node)}' \
1637 1637 > --config ui.mergemarkertemplate='uitmpl {rev}' \
1638 1638 > --config ui.mergemarkers=detailed \
1639 1639 > merge -r 2
1640 1640 merging f
1641 1641 labellocal: "working copy"
1642 1642 labelother: "merge rev"
1643 1643 output (arg): "$TESTTMP/repo/f"
1644 1644 output (contents):
1645 1645 <<<<<<< working copy: uitmpl 1
1646 1646 revision 1
1647 1647 =======
1648 1648 revision 2
1649 1649 >>>>>>> merge rev: uitmpl 2
1650 1650 space
1651 1651 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
1652 1652 (branch merge, don't forget to commit)
1653 1653 $ rm -f 'mytool'
1654 1654
1655 1655 premerge=keep uses the *tool's* mergemarkertemplate if tool's
1656 1656 mergemarkers=detailed; labellocal and labelother also use the tool's template
1657 1657
1658 1658 $ beforemerge
1659 1659 [merge-tools]
1660 1660 false.whatever=
1661 1661 true.priority=1
1662 1662 true.executable=cat
1663 1663 # hg update -C 1
1664 1664 $ cat <<EOF > mytool
1665 1665 > echo labellocal: \"\$1\"
1666 1666 > echo labelother: \"\$2\"
1667 1667 > echo "output (arg)": \"\$3\"
1668 1668 > echo "output (contents)":
1669 1669 > cat "\$3"
1670 1670 > EOF
1671 1671 $ hg --config merge-tools.true.executable='sh' \
1672 1672 > --config merge-tools.true.args='mytool $labellocal $labelother $output' \
1673 1673 > --config merge-tools.true.premerge=keep \
1674 1674 > --config merge-tools.true.mergemarkers=detailed \
1675 1675 > --config merge-tools.true.mergemarkertemplate='tooltmpl {short(node)}' \
1676 1676 > --config ui.mergemarkertemplate='uitmpl {rev}' \
1677 1677 > --config ui.mergemarkers=detailed \
1678 1678 > merge -r 2
1679 1679 merging f
1680 1680 labellocal: "working copy: tooltmpl ef83787e2614"
1681 1681 labelother: "merge rev: tooltmpl 0185f4e0cf02"
1682 1682 output (arg): "$TESTTMP/repo/f"
1683 1683 output (contents):
1684 1684 <<<<<<< working copy: tooltmpl ef83787e2614
1685 1685 revision 1
1686 1686 =======
1687 1687 revision 2
1688 1688 >>>>>>> merge rev: tooltmpl 0185f4e0cf02
1689 1689 space
1690 1690 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
1691 1691 (branch merge, don't forget to commit)
1692 1692 $ rm -f 'mytool'
1693 1693
1694 1694 Issue3581: Merging a filename that needs to be quoted
1695 1695 (This test doesn't work on Windows filesystems even on Linux, so check
1696 1696 for Unix-like permission)
1697 1697
1698 1698 #if unix-permissions
1699 1699 $ beforemerge
1700 1700 [merge-tools]
1701 1701 false.whatever=
1702 1702 true.priority=1
1703 1703 true.executable=cat
1704 1704 # hg update -C 1
1705 1705 $ echo "revision 5" > '"; exit 1; echo "'
1706 1706 $ hg commit -Am "revision 5"
1707 1707 adding "; exit 1; echo "
1708 1708 warning: filename contains '"', which is reserved on Windows: '"; exit 1; echo "'
1709 1709 $ hg update -C 1 > /dev/null
1710 1710 $ echo "revision 6" > '"; exit 1; echo "'
1711 1711 $ hg commit -Am "revision 6"
1712 1712 adding "; exit 1; echo "
1713 1713 warning: filename contains '"', which is reserved on Windows: '"; exit 1; echo "'
1714 1714 created new head
1715 1715 $ hg merge --config merge-tools.true.executable="true" -r 5
1716 1716 merging "; exit 1; echo "
1717 1717 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
1718 1718 (branch merge, don't forget to commit)
1719 1719 $ hg update -C 1 > /dev/null
1720 1720
1721 1721 #else
1722 1722
1723 1723 Match the non-portable filename commits above for test stability
1724 1724
1725 1725 $ hg import --bypass -q - << EOF
1726 1726 > # HG changeset patch
1727 1727 > revision 5
1728 1728 >
1729 1729 > diff --git a/"; exit 1; echo " b/"; exit 1; echo "
1730 1730 > new file mode 100644
1731 1731 > --- /dev/null
1732 1732 > +++ b/"; exit 1; echo "
1733 1733 > @@ -0,0 +1,1 @@
1734 1734 > +revision 5
1735 1735 > EOF
1736 1736
1737 1737 $ hg import --bypass -q - << EOF
1738 1738 > # HG changeset patch
1739 1739 > revision 6
1740 1740 >
1741 1741 > diff --git a/"; exit 1; echo " b/"; exit 1; echo "
1742 1742 > new file mode 100644
1743 1743 > --- /dev/null
1744 1744 > +++ b/"; exit 1; echo "
1745 1745 > @@ -0,0 +1,1 @@
1746 1746 > +revision 6
1747 1747 > EOF
1748 1748
1749 1749 #endif
1750 1750
1751 1751 Merge post-processing
1752 1752
1753 1753 cat is a bad merge-tool and doesn't change:
1754 1754
1755 1755 $ beforemerge
1756 1756 [merge-tools]
1757 1757 false.whatever=
1758 1758 true.priority=1
1759 1759 true.executable=cat
1760 1760 # hg update -C 1
1761 1761 $ hg merge -y -r 2 --config merge-tools.true.checkchanged=1
1762 1762 merging f
1763 1763 revision 1
1764 1764 space
1765 1765 revision 0
1766 1766 space
1767 1767 revision 2
1768 1768 space
1769 1769 output file f appears unchanged
1770 1770 was merge successful (yn)? n
1771 1771 merging f failed!
1772 1772 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
1773 1773 use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
1774 1774 [1]
1775 1775 $ aftermerge
1776 1776 # cat f
1777 1777 revision 1
1778 1778 space
1779 1779 # hg stat
1780 1780 M f
1781 1781 ? f.orig
1782 1782 # hg resolve --list
1783 1783 U f
1784 1784
1785 1785 missingbinary is a merge-tool that doesn't exist:
1786 1786
1787 1787 $ echo "missingbinary.executable=doesnotexist" >> .hg/hgrc
1788 1788 $ beforemerge
1789 1789 [merge-tools]
1790 1790 false.whatever=
1791 1791 true.priority=1
1792 1792 true.executable=cat
1793 1793 missingbinary.executable=doesnotexist
1794 1794 # hg update -C 1
1795 1795 $ hg merge -y -r 2 --config ui.merge=missingbinary
1796 1796 couldn't find merge tool missingbinary (for pattern f)
1797 1797 merging f
1798 1798 couldn't find merge tool missingbinary (for pattern f)
1799 1799 revision 1
1800 1800 space
1801 1801 revision 0
1802 1802 space
1803 1803 revision 2
1804 1804 space
1805 1805 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
1806 1806 (branch merge, don't forget to commit)
1807 1807
1808 1808 $ hg update -q -C 1
1809 1809 $ rm f
1810 1810
1811 1811 internal merge cannot handle symlinks and shouldn't try:
1812 1812
1813 1813 #if symlink
1814 1814
1815 1815 $ ln -s symlink f
1816 1816 $ hg commit -qm 'f is symlink'
1817 1817
1818 1818 #else
1819 1819
1820 1820 $ hg import --bypass -q - << EOF
1821 1821 > # HG changeset patch
1822 1822 > f is symlink
1823 1823 >
1824 1824 > diff --git a/f b/f
1825 1825 > old mode 100644
1826 1826 > new mode 120000
1827 1827 > --- a/f
1828 1828 > +++ b/f
1829 1829 > @@ -1,2 +1,1 @@
1830 1830 > -revision 1
1831 1831 > -space
1832 1832 > +symlink
1833 1833 > \ No newline at end of file
1834 1834 > EOF
1835 1835
1836 1836 Resolve 'other [destination] changed f which local [working copy] deleted' prompt
1837 1837 $ hg up -q -C --config ui.interactive=True << EOF
1838 1838 > c
1839 1839 > EOF
1840 1840
1841 1841 #endif
1842 1842
1843 1843 $ hg merge -r 2 --tool internal:merge
1844 1844 merging f
1845 1845 warning: internal :merge cannot merge symlinks for f
1846 1846 warning: conflicts while merging f! (edit, then use 'hg resolve --mark')
1847 1847 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
1848 1848 use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
1849 1849 [1]
1850 1850
1851 1851 Verify naming of temporary files and that extension is preserved:
1852 1852
1853 1853 $ hg update -q -C 1
1854 1854 $ hg mv f f.txt
1855 1855 $ hg ci -qm "f.txt"
1856 1856 $ hg update -q -C 2
1857 1857 $ hg merge -y -r tip --tool echo --config merge-tools.echo.args='$base $local $other $output'
1858 1858 merging f and f.txt to f.txt
1859 1859 */f~base.* */f~local.*.txt */f~other.*.txt $TESTTMP/repo/f.txt (glob)
1860 1860 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
1861 1861 (branch merge, don't forget to commit)
1862 1862
1863 1863 Verify naming of temporary files and that extension is preserved
1864 1864 (experimental.mergetempdirprefix version):
1865 1865
1866 1866 $ hg update -q -C 1
1867 1867 $ hg mv f f.txt
1868 1868 $ hg ci -qm "f.txt"
1869 1869 $ hg update -q -C 2
1870 1870 $ hg merge -y -r tip --tool echo \
1871 1871 > --config merge-tools.echo.args='$base $local $other $output' \
1872 1872 > --config experimental.mergetempdirprefix=$TESTTMP/hgmerge.
1873 1873 merging f and f.txt to f.txt
1874 1874 $TESTTMP/hgmerge.*/f~base $TESTTMP/hgmerge.*/f~local.txt $TESTTMP/hgmerge.*/f~other.txt $TESTTMP/repo/f.txt (glob)
1875 1875 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
1876 1876 (branch merge, don't forget to commit)
1877 1877
1878 1878 Binary files capability checking
1879 1879
1880 1880 $ hg update -q -C 0
1881 1881 $ python <<EOF
1882 1882 > with open('b', 'wb') as fp:
1883 1883 > fp.write(b'\x00\x01\x02\x03')
1884 1884 > EOF
1885 1885 $ hg add b
1886 1886 $ hg commit -qm "add binary file (#1)"
1887 1887
1888 1888 $ hg update -q -C 0
1889 1889 $ python <<EOF
1890 1890 > with open('b', 'wb') as fp:
1891 1891 > fp.write(b'\x03\x02\x01\x00')
1892 1892 > EOF
1893 1893 $ hg add b
1894 1894 $ hg commit -qm "add binary file (#2)"
1895 1895
1896 1896 By default, binary files capability of internal merge tools is not
1897 1897 checked strictly.
1898 1898
1899 1899 (for merge-patterns, chosen unintentionally)
1900 1900
1901 1901 $ hg merge 9 \
1902 1902 > --config merge-patterns.b=:merge-other \
1903 1903 > --config merge-patterns.re:[a-z]=:other
1904 1904 warning: check merge-patterns configurations, if ':merge-other' for binary file 'b' is unintentional
1905 1905 (see 'hg help merge-tools' for binary files capability)
1906 1906 merging b
1907 1907 warning: b looks like a binary file.
1908 1908 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
1909 1909 use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
1910 1910 [1]
1911 1911 $ hg merge --abort -q
1912 1912
1913 1913 (for ui.merge, ignored unintentionally)
1914 1914
1915 1915 $ hg merge 9 \
1916 1916 > --config merge-tools.:other.binary=true \
1917 1917 > --config ui.merge=:other
1918 1918 tool :other (for pattern b) can't handle binary
1919 1919 tool true can't handle binary
1920 1920 tool :other can't handle binary
1921 1921 tool false can't handle binary
1922 1922 no tool found to merge b
1923 1923 file 'b' needs to be resolved.
1924 1924 You can keep (l)ocal [working copy], take (o)ther [merge rev], or leave (u)nresolved.
1925 1925 What do you want to do? u
1926 1926 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
1927 1927 use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
1928 1928 [1]
1929 1929 $ hg merge --abort -q
1930 1930
1931 1931 With merge.strict-capability-check=true, binary files capability of
1932 1932 internal merge tools is checked strictly.
1933 1933
1934 1934 $ f --hexdump b
1935 1935 b:
1936 1936 0000: 03 02 01 00 |....|
1937 1937
1938 1938 (for merge-patterns)
1939 1939
1940 1940 $ hg merge 9 --config merge.strict-capability-check=true \
1941 1941 > --config merge-tools.:merge-other.binary=true \
1942 1942 > --config merge-patterns.b=:merge-other \
1943 1943 > --config merge-patterns.re:[a-z]=:other
1944 1944 tool :merge-other (for pattern b) can't handle binary
1945 1945 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
1946 1946 (branch merge, don't forget to commit)
1947 1947 $ f --hexdump b
1948 1948 b:
1949 1949 0000: 00 01 02 03 |....|
1950 1950 $ hg merge --abort -q
1951 1951
1952 1952 (for ui.merge)
1953 1953
1954 1954 $ hg merge 9 --config merge.strict-capability-check=true \
1955 1955 > --config ui.merge=:other
1956 1956 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
1957 1957 (branch merge, don't forget to commit)
1958 1958 $ f --hexdump b
1959 1959 b:
1960 1960 0000: 00 01 02 03 |....|
1961 1961 $ hg merge --abort -q
1962 1962
1963 1963 Check that the extra information is printed correctly
1964 1964
1965 1965 $ hg merge 9 \
1966 1966 > --config merge-tools.testecho.executable='echo' \
1967 1967 > --config merge-tools.testecho.args='merge runs here ...' \
1968 1968 > --config merge-tools.testecho.binary=True \
1969 1969 > --config ui.merge=testecho \
1970 1970 > --config ui.pre-merge-tool-output-template='\n{label("extmerge.running_merge_tool", "Running merge tool for {path} ({toolpath}):")}\n{separate("\n", extmerge_section(local), extmerge_section(base), extmerge_section(other))}\n' \
1971 1971 > --config 'templatealias.extmerge_section(sect)="- {pad("{sect.name} ({sect.label})", 20, left=True)}: {revset(sect.node)%"{rev}:{shortest(node,8)} {desc|firstline} {separate(" ", tags, bookmarks, branch)}"}"'
1972 1972 merging b
1973 1973
1974 1974 Running merge tool for b ("*/bin/echo.exe"): (glob) (windows !)
1975 1975 Running merge tool for b (*/bin/echo): (glob) (no-windows !)
1976 1976 - local (working copy): 10:2d1f533d add binary file (#2) tip default
1977 1977 - base (base): -1:00000000 default
1978 1978 - other (merge rev): 9:1e7ad7d7 add binary file (#1) default
1979 1979 merge runs here ...
1980 1980 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
1981 1981 (branch merge, don't forget to commit)
1982 1982
1983 1983 Check that debugpicktool examines which merge tool is chosen for
1984 1984 specified file as expected
1985 1985
1986 1986 $ beforemerge
1987 1987 [merge-tools]
1988 1988 false.whatever=
1989 1989 true.priority=1
1990 1990 true.executable=cat
1991 1991 missingbinary.executable=doesnotexist
1992 1992 # hg update -C 1
1993 1993
1994 1994 (default behavior: checking files in the working parent context)
1995 1995
1996 1996 $ hg manifest
1997 1997 f
1998 1998 $ hg debugpickmergetool
1999 1999 f = true
2000 2000
2001 2001 (-X/-I and file patterns limmit examination targets)
2002 2002
2003 2003 $ hg debugpickmergetool -X f
2004 2004 $ hg debugpickmergetool unknown
2005 2005 unknown: no such file in rev ef83787e2614
2006 2006
2007 2007 (--changedelete emulates merging change and delete)
2008 2008
2009 2009 $ hg debugpickmergetool --changedelete
2010 2010 f = :prompt
2011 2011
2012 2012 (-r REV causes checking files in specified revision)
2013 2013
2014 2014 $ hg manifest -r 8
2015 2015 f.txt
2016 2016 $ hg debugpickmergetool -r 8
2017 2017 f.txt = true
2018 2018
2019 2019 #if symlink
2020 2020
2021 2021 (symlink causes chosing :prompt)
2022 2022
2023 2023 $ hg debugpickmergetool -r 6d00b3726f6e
2024 2024 f = :prompt
2025 2025
2026 2026 (by default, it is assumed that no internal merge tools has symlinks
2027 2027 capability)
2028 2028
2029 2029 $ hg debugpickmergetool \
2030 2030 > -r 6d00b3726f6e \
2031 2031 > --config merge-tools.:merge-other.symlink=true \
2032 2032 > --config merge-patterns.f=:merge-other \
2033 2033 > --config merge-patterns.re:[f]=:merge-local \
2034 2034 > --config merge-patterns.re:[a-z]=:other
2035 2035 f = :prompt
2036 2036
2037 2037 $ hg debugpickmergetool \
2038 2038 > -r 6d00b3726f6e \
2039 2039 > --config merge-tools.:other.symlink=true \
2040 2040 > --config ui.merge=:other
2041 2041 f = :prompt
2042 2042
2043 2043 (with strict-capability-check=true, actual symlink capabilities are
2044 2044 checked striclty)
2045 2045
2046 2046 $ hg debugpickmergetool --config merge.strict-capability-check=true \
2047 2047 > -r 6d00b3726f6e \
2048 2048 > --config merge-tools.:merge-other.symlink=true \
2049 2049 > --config merge-patterns.f=:merge-other \
2050 2050 > --config merge-patterns.re:[f]=:merge-local \
2051 2051 > --config merge-patterns.re:[a-z]=:other
2052 2052 f = :other
2053 2053
2054 2054 $ hg debugpickmergetool --config merge.strict-capability-check=true \
2055 2055 > -r 6d00b3726f6e \
2056 2056 > --config ui.merge=:other
2057 2057 f = :other
2058 2058
2059 2059 $ hg debugpickmergetool --config merge.strict-capability-check=true \
2060 2060 > -r 6d00b3726f6e \
2061 2061 > --config merge-tools.:merge-other.symlink=true \
2062 2062 > --config ui.merge=:merge-other
2063 2063 f = :prompt
2064 2064
2065 2065 #endif
2066 2066
2067 2067 (--verbose shows some configurations)
2068 2068
2069 2069 $ hg debugpickmergetool --tool foobar -v
2070 2070 with --tool 'foobar'
2071 2071 f = foobar
2072 2072
2073 2073 $ HGMERGE=false hg debugpickmergetool -v
2074 2074 with HGMERGE='false'
2075 2075 f = false
2076 2076
2077 2077 $ hg debugpickmergetool --config ui.merge=false -v
2078 2078 with ui.merge='false'
2079 2079 f = false
2080 2080
2081 2081 (--debug shows errors detected intermediately)
2082 2082
2083 2083 $ hg debugpickmergetool --config merge-patterns.f=true --config merge-tools.true.executable=nonexistentmergetool --debug f
2084 2084 couldn't find merge tool true (for pattern f)
2085 2085 couldn't find merge tool true
2086 2086 f = false
2087 2087
2088 2088 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now