mirror of
https://github.com/gohugoio/hugo.git
synced 2026-09-02 03:32:38 +00:00
Compare commits
694 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 41c6c52ead | |||
| d270eaf4fc | |||
| b2e67505f8 | |||
| 32508045d3 | |||
| e521c9a36d | |||
| dfb1cc4315 | |||
| 0294a4a4f8 | |||
| 53a352795a | |||
| 351ed0f569 | |||
| d831d2fce8 | |||
| 634481ba8c | |||
| c19f65f956 | |||
| 022c479551 | |||
| f27e542442 | |||
| ae6cf93c84 | |||
| a70da2b74a | |||
| 805664818d | |||
| ee3d2bb1d3 | |||
| eb2a500367 | |||
| 5cb52c2315 | |||
| 30eea3915b | |||
| 8f40f34cd1 | |||
| 8ddbc95466 | |||
| 724d5db580 | |||
| 875fe4050c | |||
| 56362e4bc7 | |||
| f5cfb9e251 | |||
| 04dc469fbd | |||
| 07919d1ccb | |||
| f75f90079a | |||
| e31b1d1946 | |||
| 43a23239b2 | |||
| 4479f09c9c | |||
| 7eb0e10a80 | |||
| 24ce98b6d1 | |||
| 0019d60f67 | |||
| e451b984cf | |||
| b4d60b3db1 | |||
| 020e4acee4 | |||
| 4c34faf426 | |||
| a339f62664 | |||
| 4c8552b114 | |||
| 34e4742f0c | |||
| bffa2a2a98 | |||
| a1b0353ccb | |||
| bc6f84c581 | |||
| 80410257e3 | |||
| 6c8c0c8b6a | |||
| 49fedbc51c | |||
| 8290720107 | |||
| e0c67958f1 | |||
| f6b9ce6ebd | |||
| 4b03399b78 | |||
| 40dfdd0952 | |||
| 3a6dc6d3f4 | |||
| 6cd2110ab2 | |||
| 6a365c2712 | |||
| 19aa95fc7f | |||
| 923dd9d1c1 | |||
| d9bdd37d35 | |||
| b2eaf4c8c2 | |||
| efb6ee6c1c | |||
| 4bd65e224f | |||
| ff2266300a | |||
| 8677cfb04f | |||
| 093dacab29 | |||
| 4a9d408fe0 | |||
| 9312059888 | |||
| a7e3da242f | |||
| b70a12ec45 | |||
| 4dd90050f1 | |||
| 3d544c9ae4 | |||
| 2c4689f7b8 | |||
| 9c0860f749 | |||
| a1694b06af | |||
| 219ec38007 | |||
| be6b901cf7 | |||
| 643b671931 | |||
| ab4e1dfa4e | |||
| 02f31897b4 | |||
| 402da3f8f3 | |||
| 92405e5b0a | |||
| a074f758b0 | |||
| bb2aa08709 | |||
| 9096842b04 | |||
| 3aa7f0b27f | |||
| 9b870aa788 | |||
| 93aad3c543 | |||
| 5af045ebab | |||
| 8eafe0845d | |||
| 31fb29fb3f | |||
| d392893cd7 | |||
| a886dd53b8 | |||
| a91cd7652f | |||
| 162f41d0ef | |||
| 32ba623541 | |||
| 552cef5c57 | |||
| 73483d0f9e | |||
| 9b5debe4b8 | |||
| 12530519d8 | |||
| f55d2f4376 | |||
| 282f1aa3db | |||
| fcd63de3a5 | |||
| 150d75738b | |||
| 06d295427f | |||
| 26ae12c0c6 | |||
| ee733085b7 | |||
| 01758f99b9 | |||
| 845a7ba4fc | |||
| 10f60de89a | |||
| 785a31b5b8 | |||
| bc1e05286a | |||
| f518b4f71e | |||
| dc6b7a75ff | |||
| 3f515f0e33 | |||
| a9bcd38181 | |||
| ef0f1a7269 | |||
| abbc99d4c6 | |||
| 76c95f55a5 | |||
| 2c7f5b62f6 | |||
| c46fc838a9 | |||
| 504c78da4b | |||
| b660ea8d54 | |||
| 64f88f3011 | |||
| 7a2c10ae60 | |||
| e1c328df25 | |||
| 5afe0a57d0 | |||
| 8900e33918 | |||
| 9753e1b9c5 | |||
| ececd1b122 | |||
| b0ca723eb2 | |||
| 4c65ceccc7 | |||
| 57a471a0db | |||
| 9b63af55bd | |||
| 23fc65832c | |||
| e7b5e36e04 | |||
| a9b52b4175 | |||
| b073a1c972 | |||
| 4227cc1bd3 | |||
| c239c643fe | |||
| d7b22aee46 | |||
| 3cc4fdd6f3 | |||
| 7eb80a9e6f | |||
| 78c1a6a7c6 | |||
| f6745ad358 | |||
| f523e9f0fd | |||
| 63cd05ce5a | |||
| 902535ef11 | |||
| 70aebba04d | |||
| 3e3b7d4474 | |||
| c13d368746 | |||
| 048418ba74 | |||
| eebde0c2ac | |||
| e4dc9a82b5 | |||
| 0d86a32d8f | |||
| 65c502cc81 | |||
| 537c905ec1 | |||
| 243951ebe9 | |||
| fe2ee02802 | |||
| 316d65cd70 | |||
| b95229ab49 | |||
| 0551df090e | |||
| bca40cf0c9 | |||
| 057e5a22af | |||
| fd96f65a3d | |||
| 8f7891e70c | |||
| 07b8d9466d | |||
| d3a64708f4 | |||
| 3b56244f42 | |||
| f5d3d635e6 | |||
| 0d3c42da56 | |||
| ef34dd8f0e | |||
| bc80022e03 | |||
| 2bb9496ce2 | |||
| fda3c4d1ed | |||
| 60618210b8 | |||
| f8b064f3c0 | |||
| 4713e509b6 | |||
| 6e9d2bf0c9 | |||
| e73f7a770d | |||
| 3ddffd064d | |||
| 6fc52d185a | |||
| 73c3ae818a | |||
| 7ca118fdfd | |||
| 33d5f80592 | |||
| 509d39fa6d | |||
| 7725c41d40 | |||
| 5d36d80153 | |||
| 9b34d42bb2 | |||
| c2d8f87cfc | |||
| cbc246616e | |||
| fa432b17b3 | |||
| 0cd55c66d3 | |||
| 0d5cf256e4 | |||
| 36527576b3 | |||
| 9b83f45b6d | |||
| 7fdd2b95e2 | |||
| 3d5dbdcb1a | |||
| 8a308944e4 | |||
| 7b4ade56dd | |||
| 7c7974b711 | |||
| 5656a908d8 | |||
| 2dc222cec4 | |||
| 4d22ad580e | |||
| 5e2f128911 | |||
| 97934779ed | |||
| 9d96078439 | |||
| 1efd93c097 | |||
| 584a9b3e2e | |||
| 86b4fd35e7 | |||
| 195d108da7 | |||
| 81689af799 | |||
| c94aa5cf46 | |||
| df8bb8812f | |||
| 57d8d208ed | |||
| fc06e85082 | |||
| b725253f9e | |||
| 35dedf15c0 | |||
| 7ed56c6941 | |||
| 24c716cac3 | |||
| 137d2dab32 | |||
| ba1d0051b4 | |||
| 1b1dcf586e | |||
| f6612d8bd8 | |||
| 0a2ab3f8fe | |||
| 4d24e2a326 | |||
| 18074d0c23 | |||
| 35bfb66222 | |||
| 6d21559fb5 | |||
| ba16a14c6e | |||
| 782c79ae61 | |||
| 5afcae7e0b | |||
| 60469f429e | |||
| aed7df62a8 | |||
| cd0c5d7ef3 | |||
| e7e194435b | |||
| 9d31f650da | |||
| 88a85dcea9 | |||
| 7f8530039a | |||
| 01dd7c16af | |||
| c8f45d1d86 | |||
| b3504a0ee8 | |||
| 59d15c97d8 | |||
| 9e2d086ca7 | |||
| b65518ac63 | |||
| 0e9cd8128d | |||
| fe77f7434b | |||
| 9e99950c6e | |||
| 1b364b003f | |||
| acb9109df7 | |||
| 7d0a261981 | |||
| c60806550a | |||
| ffd9dac421 | |||
| 88b93a09dc | |||
| 29fb456c9e | |||
| 718fba7d63 | |||
| 9650e56841 | |||
| e77b2e3aa0 | |||
| b5485aeae7 | |||
| ccb822eb5a | |||
| 21e9eb18ac | |||
| ae57ba6a9d | |||
| cf3e077da3 | |||
| 66beac99c6 | |||
| 968dd7a711 | |||
| 38f29e817f | |||
| cd87813aa0 | |||
| 4e815b063e | |||
| 652a59d385 | |||
| 84f0ec7f80 | |||
| bdfbcf6f4b | |||
| b60e9279ab | |||
| a9b0fea6a3 | |||
| e8df097745 | |||
| 3a5ee0d2d6 | |||
| 4ffaeaf155 | |||
| bf55afd71f | |||
| 5f621df257 | |||
| 7118f89cf3 | |||
| e6dd312812 | |||
| 2681633db8 | |||
| 1b24728256 | |||
| 441b11beec | |||
| 4867cd1dea | |||
| 92c6c40419 | |||
| d36fd5b3ee | |||
| a7c515e1b5 | |||
| 216b00f358 | |||
| 144943798c | |||
| 440fdb0eb9 | |||
| b2a48dce58 | |||
| 35def0ae45 | |||
| 2f9dadae40 | |||
| ed3071b753 | |||
| ee9c136763 | |||
| 32b86076ee | |||
| 241b7483ea | |||
| e19a046c4b | |||
| a1fe552fc9 | |||
| a1a9f088b1 | |||
| e48ffb7635 | |||
| 07ad283f68 | |||
| 2c8b5d9165 | |||
| 0004a733c8 | |||
| 8a26ab0bc5 | |||
| 788e50ad3a | |||
| 4d2b6fc4c0 | |||
| 212e5e5542 | |||
| 4fdec67b11 | |||
| 9b681ecfbd | |||
| 792ef0f41a | |||
| 5b3fc1c673 | |||
| 63a33afee7 | |||
| 69aa3d4599 | |||
| ffbf5e45fa | |||
| 48994ea766 | |||
| 428b0b3294 | |||
| 1f7e9f7333 | |||
| cea1574023 | |||
| f9f779786e | |||
| a9718f44cd | |||
| f802bb236a | |||
| 6c29418278 | |||
| 10ae7c3210 | |||
| ae2d1bd52d | |||
| 81975f847d | |||
| 8103188b9b | |||
| 3ba147e702 | |||
| a2d146ec32 | |||
| d2d493ab5d | |||
| ce96895deb | |||
| 04b89857e1 | |||
| 21fa1e86f2 | |||
| c84ad8db82 | |||
| 4e0acb89b7 | |||
| 718e09ed4b | |||
| d90e37e0c6 | |||
| 32471b57bd | |||
| 4fc918e02c | |||
| 0ad378b09c | |||
| aebfe156fb | |||
| 907d9e9268 | |||
| 5862fd2a60 | |||
| 64789fb5dc | |||
| 32d4bf68da | |||
| 1415efdcd8 | |||
| 4e6bf7907d | |||
| 50be4370b0 | |||
| 3d2e6a30d4 | |||
| 4f1e4bb3fe | |||
| 9f1265fde4 | |||
| d162bbd799 | |||
| 17e0bbe821 | |||
| e442cf30a2 | |||
| 7e223b3baa | |||
| 8a6e706053 | |||
| 34061706e6 | |||
| 6f7633df7d | |||
| a546059a9c | |||
| b5d906e31e | |||
| fd70bdafe7 | |||
| 8f5c9a747f | |||
| e4fcb672ed | |||
| 18c13adcd4 | |||
| 20a35374a3 | |||
| caf16c2085 | |||
| b298c06e05 | |||
| 55e290af41 | |||
| 506a190a82 | |||
| fc81de6439 | |||
| fcaa324e3c | |||
| 959724f0e0 | |||
| a3012d85d1 | |||
| fc7f739275 | |||
| 78f227b664 | |||
| 5e03f644a4 | |||
| a92ef20ff6 | |||
| 0d54a84406 | |||
| 943f3c932f | |||
| 4f20bf29eb | |||
| 4c613d5d5d | |||
| 82a182e52c | |||
| dfc662b208 | |||
| 2f0917cc01 | |||
| 7565cda1ff | |||
| 347f2de67d | |||
| 210f6d38ac | |||
| 03f87f8f06 | |||
| 3437174c3a | |||
| f66302ca05 | |||
| 944150bafb | |||
| bf2837a314 | |||
| cf6131dc18 | |||
| fd62817bba | |||
| 3ebe83aea7 | |||
| 0c16debc7f | |||
| 794e1f9e72 | |||
| c64e9504d8 | |||
| b74591123e | |||
| 3b2fe3cd33 | |||
| 85e4dd7370 | |||
| 3089fc0ba1 | |||
| e10e36cf70 | |||
| ef290125c9 | |||
| 5d2fceecaf | |||
| 0696df6686 | |||
| 5ba49c8778 | |||
| c3ccda8fab | |||
| beabc8d998 | |||
| 332b65e4cc | |||
| 3553fc533a | |||
| 9cabb46f68 | |||
| 173187e263 | |||
| 8a1c637c44 | |||
| 6d95dc9d74 | |||
| 56a343507c | |||
| d48a98c477 | |||
| 3261678f63 | |||
| f465c5c307 | |||
| 3400aff258 | |||
| fdfa4a5fe6 | |||
| 8cbe2bbfad | |||
| 807db97af8 | |||
| 26eeb29147 | |||
| b886fa46bb | |||
| 14bce18a6c | |||
| 837e084bbe | |||
| 08e4f9ff9c | |||
| acfa153863 | |||
| f033d9f01d | |||
| 59fe279424 | |||
| 62119022d1 | |||
| 2f3f41f736 | |||
| 60f0725b3e | |||
| dcf70ea5a5 | |||
| 7487c0abf2 | |||
| 79a022a15c | |||
| ead5799f7e | |||
| d57be11324 | |||
| d070596694 | |||
| f5ea359dd3 | |||
| 78b26d538c | |||
| 28179bd556 | |||
| fef057b489 | |||
| 50dfe40b91 | |||
| 1ef4211fea | |||
| b148063ec0 | |||
| e9a7ebaf67 | |||
| 506820435c | |||
| 49972d0792 | |||
| c98132e30e | |||
| af19253f43 | |||
| e96234590d | |||
| d62bc74770 | |||
| 1b1d62fdc9 | |||
| 33e9d79b78 | |||
| fc6abc39c7 | |||
| 18ed22be5c | |||
| 207913f349 | |||
| 830b1a94de | |||
| 605cff4071 | |||
| 6dd60fca73 | |||
| 138a025916 | |||
| 58ac83a98c | |||
| 2b8e8e6d96 | |||
| aef8369865 | |||
| f9e798e8c4 | |||
| ee56efffcb | |||
| 2c4e76e962 | |||
| 9c7d6e475c | |||
| 4482958f96 | |||
| e1ec3bc2b2 | |||
| 1cfa63b9ec | |||
| b9318e4315 | |||
| 5e39eb20a6 | |||
| e556848805 | |||
| 634938908e | |||
| c63db7f1f6 | |||
| 5e2a547cb5 | |||
| ee090c0940 | |||
| edc5c4741c | |||
| 05e358fd33 | |||
| a2e85d9a75 | |||
| 4fba78dd0e | |||
| c011b4667f | |||
| 35348b4b34 | |||
| 34915777c2 | |||
| 0f4a837ed1 | |||
| b395d686e9 | |||
| 97987e5c02 | |||
| 111344113b | |||
| 4855c186d8 | |||
| 0c3d2b67e0 | |||
| 6f07ec7e9e | |||
| 4318dc72f8 | |||
| acdc27a32d | |||
| 3acde9ae04 | |||
| 473b6610d5 | |||
| 0bce97703c | |||
| b254532b52 | |||
| 05a2289292 | |||
| 8e553dcdef | |||
| d4fc70a3b3 | |||
| d905abc002 | |||
| 8f3946746d | |||
| b01b2564ee | |||
| 9fa5ebe2c4 | |||
| efaed306b1 | |||
| a4a7bab734 | |||
| 2a9dce423f | |||
| 30159b58e9 | |||
| cd00f7f966 | |||
| 214afe4c1b | |||
| cd830bb027 | |||
| a8458bfb25 | |||
| fef924baf1 | |||
| 07b5e602f8 | |||
| 70f16e84e2 | |||
| abadf29542 | |||
| 377ad87a51 | |||
| 292b0e26ec | |||
| 534ae9c57a | |||
| df56682a10 | |||
| be2404c8b1 | |||
| c8da8eb1f5 | |||
| 787da6bd5f | |||
| 85ba9bfffb | |||
| 9df60b62f9 | |||
| 4fad43c8bd | |||
| fb0f2cc718 | |||
| 748fd4cb0d | |||
| b82f440c59 | |||
| c8143efa5d | |||
| c80132bbe5 | |||
| 75fa4c5c95 | |||
| fd7969e0b0 | |||
| b7fa3c4bba | |||
| 6a848cbc3a | |||
| 746ba803af | |||
| 612b7d376f | |||
| 6f4ff1a461 | |||
| ddeca45933 | |||
| 31f2091f58 | |||
| d4611c4322 | |||
| 20af9a0781 | |||
| 9a1e6d15a3 | |||
| 84adecf97b | |||
| 573558a078 | |||
| 8b10c22f82 | |||
| 195bd1243b | |||
| 6a544ece24 | |||
| 4b430d456a | |||
| b9f10c75cb | |||
| 537c598e9a | |||
| 67348676f7 | |||
| f9cc0ec76e | |||
| b5483eed6e | |||
| 90285f4750 | |||
| 4949bdc2ef | |||
| f7c1b5fe1c | |||
| 183e862607 | |||
| f50ee6bbe5 | |||
| c0655ba6ce | |||
| a2dda22c36 | |||
| 910d81a692 | |||
| 8c490a73b3 | |||
| e6cd9da42d | |||
| dcf25c0b49 | |||
| 7d7771b673 | |||
| b9e4f5898b | |||
| e820b366b9 | |||
| 4055c12184 | |||
| 3ba7c92530 | |||
| 19ef27b98e | |||
| c6b661de82 | |||
| cdfd1c99ba | |||
| 047af7cfe5 | |||
| f9ebaaed1b | |||
| d39636a5fc | |||
| ad01aea3f4 | |||
| ec37420468 | |||
| ae63c2b5c9 | |||
| e627449c0a | |||
| c2235c6a62 | |||
| 4f69ade711 | |||
| f8b8b091fc | |||
| cb39847dee | |||
| 5f42590144 | |||
| f3cb0be35a | |||
| bffc4e12fe | |||
| 12f6a1cdc0 | |||
| 2fa851e650 | |||
| 21dbfa1f11 | |||
| e5591e89d3 | |||
| 88929bc23f | |||
| 850c18bfe4 | |||
| da0437b481 | |||
| 90fe00df07 | |||
| 02efadc243 | |||
| 00e00da233 | |||
| 45c665d396 | |||
| a06c06a5c2 | |||
| 0256959a35 | |||
| eded9ac2a0 | |||
| 8d72512825 | |||
| e81aef0a95 | |||
| 673e622fa9 | |||
| 28bd06265e | |||
| 9f9191471e | |||
| 3727a9d111 | |||
| 48565de62f | |||
| 808e126216 | |||
| aa85a46dc6 | |||
| 35011bcb26 | |||
| 084624baac | |||
| 15163266c1 | |||
| a74f7d3cc0 | |||
| cb84d98169 | |||
| c91dbe4ce9 | |||
| 6e0452e189 | |||
| d2b1162680 | |||
| 6265184309 | |||
| 797127010d | |||
| 1672a332da | |||
| 823ce055ed | |||
| 5f7a65a080 | |||
| 25e3da3343 | |||
| 9df98ec49c | |||
| 2fc3380707 | |||
| f1916f114b | |||
| 6aa5c9117f | |||
| 12a65e76df | |||
| 58c0f5e617 | |||
| a1c3e3c1f3 | |||
| 42e150fbfa | |||
| 028b356787 | |||
| e9f87c4e3f | |||
| defd7106bf | |||
| beb6c03bc8 | |||
| 4a3efea7ef | |||
| c66dc6c74f | |||
| 127d5feb32 | |||
| 2d42ba912b | |||
| 5b7b5dea1f | |||
| ccfaeb678b | |||
| 057b1377c5 | |||
| 48dbb593f7 | |||
| f0266e2ef3 | |||
| 77aa385b84 | |||
| 545a1c1ced | |||
| 47aaa52e39 | |||
| 428907cc34 | |||
| a78b3e3415 | |||
| cfcb014515 | |||
| 0b579db80f | |||
| ee5d027cd5 | |||
| 4a340ba254 | |||
| 3466884e36 | |||
| 6ff435aa3f | |||
| 82abca32fa | |||
| fc045e12a9 | |||
| 9679023f2b | |||
| 0a9172672a | |||
| efa74c5c6e | |||
| 6408c1cbc8 | |||
| 522ba1cd98 | |||
| 889dc47ceb | |||
| f720fe56db | |||
| d6ed17c60f | |||
| 145b3fcce3 | |||
| 01e249e97c | |||
| 4b560cc119 | |||
| e3e627e6b7 | |||
| 83d03a5201 | |||
| fc0f13b684 | |||
| 7eeebe1e5a | |||
| 740fa4a91d | |||
| f8c67f93e1 | |||
| 3d9235e8fc | |||
| f7d909f394 | |||
| 8a7ef3cf4e | |||
| 2dfe242ea6 | |||
| 7a1464e547 | |||
| 41d50b4dd3 | |||
| 4d53ae697e | |||
| 626b16e024 | |||
| 9e1dcefc5f | |||
| 2919a6a503 | |||
| 432885c499 | |||
| 6a3e89743c | |||
| 9613e3e8a8 | |||
| c950c86b4e | |||
| 915202494b | |||
| b3e4f911f4 |
@@ -1,6 +1,6 @@
|
||||
defaults: &defaults
|
||||
docker:
|
||||
- image: bepsays/ci-goreleaser:1.14.3
|
||||
- image: bepsays/ci-goreleaser:1.16.5-2
|
||||
environment:
|
||||
CGO_ENABLED: "0"
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: 'Bug report'
|
||||
labels: ''
|
||||
labels: 'bug'
|
||||
assignees: ''
|
||||
about: Create a report to help us improve
|
||||
---
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: SUPPORT, ISSUES and TROUBLESHOOTING
|
||||
url: https://discourse.gohugo.io/
|
||||
about: Please DO NOT use Github for support requests. Please visit https://discourse.gohugo.io for support! You will be helped much faster there. If you ignore this request your issue might be closed with a discourse label.
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
name: Support (Do not use)
|
||||
about: Please do not use Github for support requests. Visit https://discourse.gohugo.io for support
|
||||
title: ''
|
||||
labels: support
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Issues created with this template will be automatically closed. Please visit https://discourse.gohugo.io for the support you really, really, want!
|
||||
@@ -0,0 +1,7 @@
|
||||
# See https://docs.github.com/en/github/administering-a-repository/configuration-options-for-dependency-updates#package-ecosystem
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
@@ -1,14 +0,0 @@
|
||||
on:
|
||||
schedule:
|
||||
- cron: 0 5 * * 3
|
||||
name: Weekly Issue Closure
|
||||
jobs:
|
||||
cycle-weekly-close:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: weekly-issue-closure
|
||||
uses: bdougie/close-issues-based-on-label@master
|
||||
env:
|
||||
LABEL: support
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,82 @@
|
||||
on: [push, pull_request]
|
||||
name: Test
|
||||
jobs:
|
||||
test:
|
||||
env:
|
||||
GOPROXY: https://proxy.golang.org
|
||||
GO111MODULE: on
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.16.x]
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@37335c7bb261b353407cff977110895fa0b4f7d8
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
- name: Install Ruby
|
||||
uses: actions/setup-ruby@5f29a1cd8dfebf420691c4c9a0e832e2fae5a526
|
||||
with:
|
||||
ruby-version: '2.7'
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@3105fb18c05ddd93efea5f9e0bef7a03a6e9e7df
|
||||
with:
|
||||
python-version: '3.x'
|
||||
- name: Install Mage
|
||||
run: go get github.com/magefile/mage@07afc7d24f4d6d6442305d49552f04fbda5ccb3e
|
||||
- name: Install asciidoctor
|
||||
uses: reitzig/actions-asciidoctor@7570212ae20b63653481675fb1ff62d1073632b0
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
||||
- name: Install docutils
|
||||
run: |
|
||||
pip install docutils
|
||||
rst2html.py --version
|
||||
- if: matrix.os == 'ubuntu-latest'
|
||||
name: Install pandoc on Linux
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y pandoc
|
||||
- if: matrix.os == 'macos-latest'
|
||||
run: |
|
||||
brew install pandoc
|
||||
- if: matrix.os == 'windows-latest'
|
||||
run: |
|
||||
choco install pandoc
|
||||
- run: pandoc -v
|
||||
- if: matrix.os == 'ubuntu-latest'
|
||||
name: Install dart-sass-embedded Linux
|
||||
run: |
|
||||
curl -LJO https://github.com/sass/dart-sass-embedded/releases/download/1.0.0-beta.6/sass_embedded-1.0.0-beta.6-linux-x64.tar.gz;
|
||||
echo "04fc1e5e28d29a4585a701941b6dace56771d94bfbe7f9e4db28d24417ceeec3 sass_embedded-1.0.0-beta.6-linux-x64.tar.gz" | sha256sum -c;
|
||||
tar -xvf sass_embedded-1.0.0-beta.6-linux-x64.tar.gz;
|
||||
echo "$GITHUB_WORKSPACE/sass_embedded/" >> $GITHUB_PATH
|
||||
- if: matrix.os == 'macos-latest'
|
||||
name: Install dart-sass-embedded MacOS
|
||||
run: |
|
||||
curl -LJO https://github.com/sass/dart-sass-embedded/releases/download/1.0.0-beta.6/sass_embedded-1.0.0-beta.6-macos-x64.tar.gz;
|
||||
echo "b3b984675a9b04aa22f6f2302dda4191b507ac2ca124467db2dfe7e58e72fbad sass_embedded-1.0.0-beta.6-macos-x64.tar.gz" | shasum -a 256 -c;
|
||||
tar -xvf sass_embedded-1.0.0-beta.6-macos-x64.tar.gz;
|
||||
echo "$GITHUB_WORKSPACE/sass_embedded/" >> $GITHUB_PATH
|
||||
- if: matrix.os == 'windows-latest'
|
||||
name: Install dart-sass-embedded Windows
|
||||
run: |
|
||||
curl -LJO https://github.com/sass/dart-sass-embedded/releases/download/1.0.0-beta.6/sass_embedded-1.0.0-beta.6-windows-x64.zip;
|
||||
echo "6ae442129dbb3334bc21ef851261da6c0c1b560da790ca2e1350871d00ab816d sass_embedded-1.0.0-beta.6-windows-x64.zip" | sha256sum -c;
|
||||
unzip sass_embedded-1.0.0-beta.6-windows-x64.zip;
|
||||
echo "$env:GITHUB_WORKSPACE/sass_embedded/" | Out-File -FilePath $Env:GITHUB_PATH -Encoding utf-8 -Append
|
||||
- name: Test
|
||||
run: |
|
||||
mage -v test
|
||||
mage -v check;
|
||||
- name: Build Docs
|
||||
env:
|
||||
HUGO_BUILD_TAGS: extended
|
||||
HUGO_TIMEOUT: 31000
|
||||
HUGO_IGNOREERRORS: error-remote-getjson
|
||||
HUGO_SERVICES_INSTAGRAM_ACCESSTOKEN: dummytoken
|
||||
run: |
|
||||
mage -v hugo
|
||||
./hugo -s docs/
|
||||
./hugo --renderToMemory -s docs/
|
||||
-77
@@ -1,77 +0,0 @@
|
||||
language: go
|
||||
|
||||
dist: bionic
|
||||
|
||||
env:
|
||||
global:
|
||||
- CACHE_NAME=${TRAVIS_ARCH}
|
||||
- GO111MODULE=on
|
||||
- GOPROXY=https://proxy.golang.org
|
||||
- HUGO_BUILD_TAGS=extended
|
||||
|
||||
git:
|
||||
depth: false
|
||||
go:
|
||||
- "1.13.11"
|
||||
- "1.14.3"
|
||||
- master
|
||||
|
||||
arch:
|
||||
- amd64
|
||||
- arm64
|
||||
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
- windows
|
||||
|
||||
jobs:
|
||||
allow_failures:
|
||||
- go: master
|
||||
- arch: arm64
|
||||
fast_finish: true
|
||||
exclude:
|
||||
- os: windows
|
||||
go: master
|
||||
- arch: arm64
|
||||
os: osx
|
||||
- arch: arm64
|
||||
os: windows
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/gopath/pkg/mod
|
||||
- $HOME/.cache/go-build
|
||||
- $HOME/Library/Caches/go-build
|
||||
- $HOME/AppData/Local/go-build
|
||||
|
||||
before_install:
|
||||
- df -h
|
||||
# https://travis-ci.community/t/go-cant-find-gcc-with-go1-11-1-on-windows/293/5
|
||||
- if [ "$TRAVIS_OS_NAME" = "windows" ]; then
|
||||
choco install mingw -y;
|
||||
export PATH=/c/tools/mingw64/bin:"$PATH";
|
||||
fi
|
||||
- gem install asciidoctor
|
||||
- type asciidoctor
|
||||
|
||||
install:
|
||||
- mkdir -p $HOME/src
|
||||
- mv $TRAVIS_BUILD_DIR $HOME/src
|
||||
- export TRAVIS_BUILD_DIR=$HOME/src/hugo
|
||||
- cd $HOME/src/hugo
|
||||
- go get github.com/magefile/mage
|
||||
|
||||
script:
|
||||
- go mod download
|
||||
- go mod verify
|
||||
- mage -v test
|
||||
- if [ "$TRAVIS_ARCH" = "amd64" ]; then
|
||||
mage -v check;
|
||||
else
|
||||
HUGO_TIMEOUT=30000 mage -v check;
|
||||
fi
|
||||
- mage -v hugo
|
||||
- ./hugo -s docs/
|
||||
- ./hugo --renderToMemory -s docs/
|
||||
- df -h
|
||||
+4
-2
@@ -50,7 +50,9 @@ Hugo has become a fully featured static site generator, so any new functionality
|
||||
|
||||
If it is of some complexity, the contributor is expected to maintain and support the new feature in the future (answer questions on the forum, fix any bugs etc.).
|
||||
|
||||
It is recommended to open up a discussion on the [Hugo Forum](https://discourse.gohugo.io/) to get feedback on your idea before you begin. If you are submitting a complex feature, create a small design proposal on the [Hugo issue tracker](https://github.com/gohugoio/hugo/issues) before you start.
|
||||
It is recommended to open up a discussion on the [Hugo Forum](https://discourse.gohugo.io/) to get feedback on your idea before you begin.
|
||||
|
||||
Any non-trivial code change needs to update an open [issue](https://github.com/gohugoio/hugo/issues). A non-trivial code change without an issue reference with one of the labels `bug` or `enhancement` will not be merged.
|
||||
|
||||
Note that we do not accept new features that require [CGO](https://github.com/golang/go/wiki/cgo).
|
||||
We have one exception to this rule which is LibSASS.
|
||||
@@ -144,7 +146,7 @@ Now, to make a change to Hugo's source:
|
||||
1. Add your fork as a new remote (the remote name, "fork" in this example, is arbitrary):
|
||||
|
||||
```bash
|
||||
git remote add fork git://github.com/USERNAME/hugo.git
|
||||
git remote add fork git@github.com:USERNAME/hugo.git
|
||||
```
|
||||
|
||||
1. Push the changes to your new remote:
|
||||
|
||||
+3
-3
@@ -2,9 +2,9 @@
|
||||
# Twitter: https://twitter.com/gohugoio
|
||||
# Website: https://gohugo.io/
|
||||
|
||||
FROM golang:1.13-alpine AS build
|
||||
FROM golang:1.16-alpine AS build
|
||||
|
||||
# Optionally set HUGO_BUILD_TAGS to "extended" when building like so:
|
||||
# Optionally set HUGO_BUILD_TAGS to "extended" or "nodeploy" when building like so:
|
||||
# docker build --build-arg HUGO_BUILD_TAGS=extended .
|
||||
ARG HUGO_BUILD_TAGS
|
||||
|
||||
@@ -26,7 +26,7 @@ RUN mage hugo && mage install
|
||||
|
||||
# ---
|
||||
|
||||
FROM alpine:3.11
|
||||
FROM alpine:3.12
|
||||
|
||||
COPY --from=build /go/bin/hugo /usr/bin/hugo
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ A Fast and Flexible Static Site Generator built with love by [bep](https://githu
|
||||
[Twitter](https://twitter.com/gohugoio)
|
||||
|
||||
[](https://godoc.org/github.com/gohugoio/hugo)
|
||||
[](https://travis-ci.org/gohugoio/hugo)
|
||||
[](https://github.com/gohugoio/hugo/actions?query=workflow%3ATest)
|
||||
[](https://goreportcard.com/report/github.com/gohugoio/hugo)
|
||||
|
||||
## Overview
|
||||
@@ -54,7 +54,7 @@ Use the [installation instructions in the Hugo documentation](https://gohugo.io/
|
||||
#### Prerequisite Tools
|
||||
|
||||
* [Git](https://git-scm.com/)
|
||||
* [Go (we test it with the last 2 major versions)](https://golang.org/dl/)
|
||||
* [Go (we test it with the last 2 major versions; but note that Hugo 0.81.0 only builds with >= Go 1.16.)](https://golang.org/dl/)
|
||||
|
||||
#### Fetch from GitHub
|
||||
|
||||
@@ -69,7 +69,13 @@ go install
|
||||
```
|
||||
|
||||
**If you are a Windows user, substitute the `$HOME` environment variable above with `%USERPROFILE%`.**
|
||||
|
||||
|
||||
If you want to compile with Sass/SCSS support use `--tags extended` and make sure `CGO_ENABLED=1` is set in your go environment. If you don't want to have CGO enabled, you may use the following command to temporarily enable CGO only for hugo compilation:
|
||||
|
||||
```bash
|
||||
CGO_ENABLED=1 go install --tags extended
|
||||
```
|
||||
|
||||
## The Hugo Documentation
|
||||
|
||||
The Hugo documentation now lives in its own repository, see https://github.com/gohugoio/hugoDocs. But we do keep a version of that documentation as a `git subtree` in this repository. To build the sub folder `/docs` as a Hugo site, you need to clone this repo:
|
||||
@@ -120,71 +126,101 @@ For a complete guide to contributing code to Hugo, see the [Contribution Guide](
|
||||
|
||||
## Dependencies
|
||||
|
||||
Hugo stands on the shoulder of many great open source libraries, in lexical order:
|
||||
Hugo stands on the shoulder of many great open source libraries.
|
||||
|
||||
If you run `hugo env -v` you will get a complete and up to date list.
|
||||
|
||||
In Hugo 0.83.0 that list is, in lexical order:
|
||||
|
||||
```json
|
||||
cloud.google.com/go/storage="v1.10.0"
|
||||
cloud.google.com/go="v0.81.0"
|
||||
github.com/Azure/azure-pipeline-go="v0.2.2"
|
||||
github.com/Azure/azure-storage-blob-go="v0.9.0"
|
||||
github.com/BurntSushi/locker="v0.0.0-20171006230638-a6e239ea1c69"
|
||||
github.com/BurntSushi/toml="v0.3.1"
|
||||
github.com/PuerkitoBio/purell="v1.1.1"
|
||||
github.com/PuerkitoBio/urlesc="v0.0.0-20170810143723-de5bf2ad4578"
|
||||
github.com/alecthomas/chroma="v0.9.1"
|
||||
github.com/armon/go-radix="v1.0.0"
|
||||
github.com/aws/aws-sdk-go="v1.38.23"
|
||||
github.com/bep/debounce="v1.2.0"
|
||||
github.com/bep/gitmap="v1.1.2"
|
||||
github.com/bep/godartsass="v0.12.0"
|
||||
github.com/bep/gowebp="v0.1.0"
|
||||
github.com/bep/tmc="v0.5.1"
|
||||
github.com/cli/safeexec="v1.0.0"
|
||||
github.com/cpuguy83/go-md2man/v2="v2.0.0"
|
||||
github.com/danwakefield/fnmatch="v0.0.0-20160403171240-cbb64ac3d964"
|
||||
github.com/disintegration/gift="v1.2.1"
|
||||
github.com/dlclark/regexp2="v1.4.0"
|
||||
github.com/dustin/go-humanize="v1.0.0"
|
||||
github.com/evanw/esbuild="v0.11.16"
|
||||
github.com/fsnotify/fsnotify="v1.4.9"
|
||||
github.com/getkin/kin-openapi="v0.61.0"
|
||||
github.com/ghodss/yaml="v1.0.0"
|
||||
github.com/go-openapi/jsonpointer="v0.19.5"
|
||||
github.com/go-openapi/swag="v0.19.5"
|
||||
github.com/gobuffalo/flect="v0.2.2"
|
||||
github.com/gobwas/glob="v0.2.3"
|
||||
github.com/gohugoio/go-i18n/v2="v2.1.3-0.20210430103248-4c28c89f8013"
|
||||
github.com/golang/groupcache="v0.0.0-20200121045136-8c9f03a8e57e"
|
||||
github.com/golang/protobuf="v1.5.2"
|
||||
github.com/google/go-cmp="v0.5.5"
|
||||
github.com/google/uuid="v1.1.2"
|
||||
github.com/google/wire="v0.4.0"
|
||||
github.com/googleapis/gax-go/v2="v2.0.5"
|
||||
github.com/googleapis/gax-go="v2.0.2+incompatible"
|
||||
github.com/gorilla/websocket="v1.4.2"
|
||||
github.com/hashicorp/hcl="v1.0.0"
|
||||
github.com/jdkato/prose="v1.2.1"
|
||||
github.com/jmespath/go-jmespath="v0.4.0"
|
||||
github.com/kyokomi/emoji/v2="v2.2.8"
|
||||
github.com/magiconair/properties="v1.8.1"
|
||||
github.com/mailru/easyjson="v0.0.0-20190626092158-b2ccc519800e"
|
||||
github.com/mattn/go-ieproxy="v0.0.1"
|
||||
github.com/mattn/go-isatty="v0.0.12"
|
||||
github.com/mattn/go-runewidth="v0.0.9"
|
||||
github.com/miekg/mmark="v1.3.6"
|
||||
github.com/mitchellh/hashstructure="v1.0.0"
|
||||
github.com/mitchellh/mapstructure="v1.3.3"
|
||||
github.com/muesli/smartcrop="v0.3.0"
|
||||
github.com/niklasfasching/go-org="v1.5.0"
|
||||
github.com/olekukonko/tablewriter="v0.0.5"
|
||||
github.com/pelletier/go-toml="v1.9.0"
|
||||
github.com/pkg/errors="v0.9.1"
|
||||
github.com/rogpeppe/go-internal="v1.8.0"
|
||||
github.com/russross/blackfriday/v2="v2.0.1"
|
||||
github.com/russross/blackfriday="v1.5.3-0.20200218234912-41c5fccfd6f6"
|
||||
github.com/rwcarlsen/goexif="v0.0.0-20190401172101-9e8deecbddbd"
|
||||
github.com/sanity-io/litter="v1.5.0"
|
||||
github.com/shurcooL/sanitized_anchor_name="v1.0.0"
|
||||
github.com/spf13/afero="v1.6.0"
|
||||
github.com/spf13/cast="v1.3.1"
|
||||
github.com/spf13/cobra="v1.1.3"
|
||||
github.com/spf13/fsync="v0.9.0"
|
||||
github.com/spf13/jwalterweatherman="v1.1.0"
|
||||
github.com/spf13/pflag="v1.0.5"
|
||||
github.com/spf13/viper="v1.7.1"
|
||||
github.com/subosito/gotenv="v1.2.0"
|
||||
github.com/tdewolff/minify/v2="v2.9.16"
|
||||
github.com/tdewolff/parse/v2="v2.5.14"
|
||||
github.com/yuin/goldmark-highlighting="v0.0.0-20200307114337-60d527fdb691"
|
||||
github.com/yuin/goldmark="v1.3.5"
|
||||
go.opencensus.io="v0.23.0"
|
||||
gocloud.dev="v0.20.0"
|
||||
golang.org/x/image="v0.0.0-20210220032944-ac19c3e999fb"
|
||||
golang.org/x/net="v0.0.0-20210316092652-d523dce5a7f4"
|
||||
golang.org/x/oauth2="v0.0.0-20210413134643-5e61552d6c78"
|
||||
golang.org/x/sync="v0.0.0-20210220032951-036812b2e83c"
|
||||
golang.org/x/sys="v0.0.0-20210412220455-f1c623a9e750"
|
||||
golang.org/x/text="v0.3.6"
|
||||
golang.org/x/xerrors="v0.0.0-20200804184101-5ec99f83aff1"
|
||||
google.golang.org/api="v0.45.0"
|
||||
google.golang.org/genproto="v0.0.0-20210413151531-c14fb6ef47c3"
|
||||
google.golang.org/grpc="v1.37.0"
|
||||
google.golang.org/protobuf="v1.26.0"
|
||||
gopkg.in/ini.v1="v1.51.1"
|
||||
gopkg.in/yaml.v2="v2.4.0"
|
||||
|
||||
|
||||
| Dependency | License |
|
||||
| :------------- | :------------- |
|
||||
| [github.com/alecthomas/chroma](https://github.com/alecthomas/chroma) | MIT License |
|
||||
| [github.com/armon/go-radix](https://github.com/armon/go-radix) | MIT License |
|
||||
| [github.com/aws/aws-sdk-go](https://github.com/aws/aws-sdk-go) | Apache License 2.0 |
|
||||
| [github.com/bep/debounce](https://github.com/bep/debounce) | MIT License |
|
||||
| [github.com/bep/gitmap](https://github.com/bep/gitmap) | MIT License |
|
||||
| [github.com/bep/golibsass](https://github.com/bep/golibsass) | MIT License |
|
||||
| [github.com/bep/tmc](https://github.com/bep/tmc) | MIT License |
|
||||
| [github.com/BurntSushi/locker](https://github.com/BurntSushi/locker) | The Unlicense |
|
||||
| [github.com/BurntSushi/toml](https://github.com/BurntSushi/toml) | MIT License |
|
||||
| [github.com/cpuguy83/go-md2man](https://github.com/cpuguy83/go-md2man) | MIT License |
|
||||
| [github.com/danwakefield/fnmatch](https://github.com/danwakefield/fnmatch) | BSD 2-Clause "Simplified" License |
|
||||
| [github.com/disintegration/gift](https://github.com/disintegration/gift) | MIT License |
|
||||
| [github.com/dustin/go-humanize](https://github.com/dustin/go-humanize) | MIT License |
|
||||
| [github.com/fsnotify/fsnotify](https://github.com/fsnotify/fsnotify) | BSD 3-Clause "New" or "Revised" License |
|
||||
| [github.com/gobwas/glob](https://github.com/gobwas/glob) | MIT License |
|
||||
| [github.com/gorilla/websocket](https://github.com/gorilla/websocket) | BSD 2-Clause "Simplified" License |
|
||||
| [github.com/hashicorp/golang-lru](https://github.com/hashicorp/golang-lru) | Mozilla Public License 2.0 |
|
||||
| [github.com/hashicorp/hcl](https://github.com/hashicorp/hcl) | Mozilla Public License 2.0 |
|
||||
| [github.com/jdkato/prose](https://github.com/jdkato/prose) | MIT License |
|
||||
| [github.com/kr/pretty](https://github.com/kr/pretty) | MIT License |
|
||||
| [github.com/kyokomi/emoji](https://github.com/kyokomi/emoji) | MIT License |
|
||||
| [github.com/magiconair/properties](https://github.com/magiconair/properties) | BSD 2-Clause "Simplified" License |
|
||||
| [github.com/markbates/inflect](https://github.com/markbates/inflect) | MIT License |
|
||||
| [github.com/mattn/go-isatty](https://github.com/mattn/go-isatty) | MIT License |
|
||||
| [github.com/mattn/go-runewidth](https://github.com/mattn/go-runewidth) | MIT License |
|
||||
| [github.com/miekg/mmark](https://github.com/miekg/mmark) | Simplified BSD License |
|
||||
| [github.com/mitchellh/hashstructure](https://github.com/mitchellh/hashstructure) | MIT License |
|
||||
| [github.com/mitchellh/mapstructure](https://github.com/mitchellh/mapstructure) | MIT License |
|
||||
| [github.com/muesli/smartcrop](https://github.com/muesli/smartcrop) | MIT License |
|
||||
| [github.com/nicksnyder/go-i18n](https://github.com/nicksnyder/go-i18n) | MIT License |
|
||||
| [github.com/niklasfasching/go-org](https://github.com/niklasfasching/go-org) | MIT License |
|
||||
| [github.com/olekukonko/tablewriter](https://github.com/olekukonko/tablewriter) | MIT License |
|
||||
| [github.com/pelletier/go-toml](https://github.com/pelletier/go-toml) | MIT License |
|
||||
| [github.com/pkg/errors](https://github.com/pkg/errors) | BSD 2-Clause "Simplified" License |
|
||||
| [github.com/PuerkitoBio/purell](https://github.com/PuerkitoBio/purell) | BSD 3-Clause "New" or "Revised" License |
|
||||
| [github.com/PuerkitoBio/urlesc](https://github.com/PuerkitoBio/urlesc) | BSD 3-Clause "New" or "Revised" License |
|
||||
| [github.com/rogpeppe/go-internal](https://github.com/rogpeppe/go-internal) | BSD 3-Clause "New" or "Revised" License |
|
||||
| [github.com/russross/blackfriday](https://github.com/russross/blackfriday) | Simplified BSD License |
|
||||
| [github.com/rwcarlsen/goexif](https://github.com/rwcarlsen/goexif) | BSD 2-Clause "Simplified" License |
|
||||
| [github.com/spf13/afero](https://github.com/spf13/afero) | Apache License 2.0 |
|
||||
| [github.com/spf13/cast](https://github.com/spf13/cast) | MIT License |
|
||||
| [github.com/spf13/cobra](https://github.com/spf13/cobra) | Apache License 2.0 |
|
||||
| [github.com/spf13/fsync](https://github.com/spf13/fsync) | MIT License |
|
||||
| [github.com/spf13/jwalterweatherman](https://github.com/spf13/jwalterweatherman) | MIT License |
|
||||
| [github.com/spf13/pflag](https://github.com/spf13/pflag) | BSD 3-Clause "New" or "Revised" License |
|
||||
| [github.com/spf13/viper](https://github.com/spf13/viper) | MIT License |
|
||||
| [github.com/tdewolff/minify](https://github.com/tdewolff/minify) | MIT License |
|
||||
| [github.com/tdewolff/parse](https://github.com/tdewolff/parse) | MIT License |
|
||||
| [github.com/yuin/goldmark](https://github.com/yuin/goldmark) | MIT License |
|
||||
| [github.com/yuin/goldmark-highlighting](https://github.com/yuin/goldmark-highlighting) | MIT License |
|
||||
| [go.opencensus.io](https://go.opencensus.io) | Apache License 2.0 |
|
||||
| [go.uber.org/atomic](https://go.uber.org/atomic) | MIT License |
|
||||
| [gocloud.dev](https://gocloud.dev) | Apache License 2.0 |
|
||||
| [golang.org/x/image](https://golang.org/x/image) | BSD 3-Clause "New" or "Revised" License |
|
||||
| [golang.org/x/net](https://golang.org/x/net) | BSD 3-Clause "New" or "Revised" License |
|
||||
| [golang.org/x/oauth2](https://golang.org/x/oauth2) | BSD 3-Clause "New" or "Revised" License |
|
||||
| [golang.org/x/sync](https://golang.org/x/sync) | BSD 3-Clause "New" or "Revised" License |
|
||||
| [golang.org/x/sys](https://golang.org/x/sys) | BSD 3-Clause "New" or "Revised" License |
|
||||
| [golang.org/x/text](https://golang.org/x/text) | BSD 3-Clause "New" or "Revised" License |
|
||||
| [golang.org/x/xerrors](https://golang.org/x/xerrors) | BSD 3-Clause "New" or "Revised" License |
|
||||
| [google.golang.org/api](https://google.golang.org/api) | BSD 3-Clause "New" or "Revised" License |
|
||||
| [google.golang.org/genproto](https://google.golang.org/genproto) | Apache License 2.0 |
|
||||
| [gopkg.in/ini.v1](https://gopkg.in/ini.v1) | Apache License 2.0 |
|
||||
| [gopkg.in/yaml.v2](https://gopkg.in/yaml.v2) | Apache License 2.0 |
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
## Security Policy
|
||||
|
||||
### Reporting a Vulnerability
|
||||
|
||||
Please report (suspected) security vulnerabilities to **[bjorn.erik.pedersen@gmail.com](mailto:bjorn.erik.pedersen@gmail.com)**. You will receive a response from us within 48 hours. If we can confirm the issue, we will release a patch as soon as possible depending on the complexity of the issue but historically within days.
|
||||
|
||||
Also see [Hugo's Security Model](https://gohugo.io/about/security-model/).
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
# allow user to override go executable by running as GOEXE=xxx make ...
|
||||
GOEXE="${GOEXE-go}"
|
||||
|
||||
# Send in a regexp mathing the benchmarks you want to run, i.e. './benchSite.sh "YAML"'.
|
||||
# Send in a regexp matching the benchmarks you want to run, i.e. './benchSite.sh "YAML"'.
|
||||
# Note the quotes, which will be needed for more complex expressions.
|
||||
# The above will run all variations, but only for front matter YAML.
|
||||
|
||||
|
||||
Vendored
+1
-6
@@ -158,7 +158,6 @@ func (c *Cache) ReadOrCreate(id string,
|
||||
err = create(info, f)
|
||||
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
// GetOrCreate tries to get the file with the given id from cache. If not found or expired, create will
|
||||
@@ -220,10 +219,9 @@ func (c *Cache) GetOrCreateBytes(id string, create func() ([]byte, error)) (Item
|
||||
return info, nil, err
|
||||
}
|
||||
return info, b, nil
|
||||
|
||||
}
|
||||
|
||||
// GetBytes gets the file content with the given id from the cahce, nil if none found.
|
||||
// GetBytes gets the file content with the given id from the cache, nil if none found.
|
||||
func (c *Cache) GetBytes(id string) (ItemInfo, []byte, error) {
|
||||
id = cleanID(id)
|
||||
|
||||
@@ -276,7 +274,6 @@ func (c *Cache) getOrRemove(id string) hugio.ReadSeekCloser {
|
||||
}
|
||||
|
||||
f, err := c.Fs.Open(id)
|
||||
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -299,7 +296,6 @@ func (c *Cache) getString(id string) string {
|
||||
defer c.nlocker.Unlock(id)
|
||||
|
||||
f, err := c.Fs.Open(id)
|
||||
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
@@ -307,7 +303,6 @@ func (c *Cache) getString(id string) string {
|
||||
|
||||
b, _ := ioutil.ReadAll(f)
|
||||
return string(b)
|
||||
|
||||
}
|
||||
|
||||
// Caches is a named set of caches.
|
||||
|
||||
Vendored
+6
-1
@@ -19,6 +19,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
|
||||
"github.com/gohugoio/hugo/config"
|
||||
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
@@ -123,6 +125,9 @@ func DecodeConfig(fs afero.Fs, cfg config.Provider) (Configs, error) {
|
||||
_, isOsFs := fs.(*afero.OsFs)
|
||||
|
||||
for k, v := range m {
|
||||
if _, ok := v.(maps.Params); !ok {
|
||||
continue
|
||||
}
|
||||
cc := defaultCacheConfig
|
||||
|
||||
dc := &mapstructure.DecoderConfig{
|
||||
@@ -137,7 +142,7 @@ func DecodeConfig(fs afero.Fs, cfg config.Provider) (Configs, error) {
|
||||
}
|
||||
|
||||
if err := decoder.Decode(v); err != nil {
|
||||
return nil, err
|
||||
return nil, errors.Wrap(err, "failed to decode filecache config")
|
||||
}
|
||||
|
||||
if cc.Dir == "" {
|
||||
|
||||
+2
-6
@@ -25,7 +25,6 @@ import (
|
||||
"github.com/gohugoio/hugo/config"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func TestDecodeConfig(t *testing.T) {
|
||||
@@ -69,7 +68,6 @@ dir = "/path/to/c3"
|
||||
c3 := decoded["images"]
|
||||
c.Assert(c3.MaxAge, qt.Equals, time.Duration(-1))
|
||||
c.Assert(c3.Dir, qt.Equals, filepath.FromSlash("/path/to/c3/filecache/images"))
|
||||
|
||||
}
|
||||
|
||||
func TestDecodeConfigIgnoreCache(t *testing.T) {
|
||||
@@ -110,7 +108,6 @@ dir = "/path/to/c3"
|
||||
for _, v := range decoded {
|
||||
c.Assert(v.MaxAge, qt.Equals, time.Duration(0))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestDecodeConfigDefault(t *testing.T) {
|
||||
@@ -178,11 +175,10 @@ dir = "/"
|
||||
|
||||
_, err = DecodeConfig(fs, cfg)
|
||||
c.Assert(err, qt.Not(qt.IsNil))
|
||||
|
||||
}
|
||||
|
||||
func newTestConfig() *viper.Viper {
|
||||
cfg := viper.New()
|
||||
func newTestConfig() config.Provider {
|
||||
cfg := config.New()
|
||||
cfg.Set("workingDir", filepath.FromSlash("/my/cool/hugoproject"))
|
||||
cfg.Set("contentDir", "content")
|
||||
cfg.Set("dataDir", "data")
|
||||
|
||||
Vendored
-2
@@ -110,7 +110,6 @@ func (c *Cache) Prune(force bool) (int, error) {
|
||||
}
|
||||
|
||||
func (c *Cache) pruneRootDir(force bool) (int, error) {
|
||||
|
||||
info, err := c.Fs.Stat(c.pruneAllRootDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
@@ -124,5 +123,4 @@ func (c *Cache) pruneRootDir(force bool) (int, error) {
|
||||
}
|
||||
|
||||
return hugofs.MakeReadableAndRemoveAllModulePkgDir(c.Fs, c.pruneAllRootDir)
|
||||
|
||||
}
|
||||
|
||||
-1
@@ -107,5 +107,4 @@ dir = ":resourceDir/_gen"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Vendored
+4
-4
@@ -25,6 +25,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gobwas/glob"
|
||||
|
||||
"github.com/gohugoio/hugo/langs"
|
||||
"github.com/gohugoio/hugo/modules"
|
||||
|
||||
@@ -181,7 +183,6 @@ dir = ":cacheDir/c"
|
||||
c.Assert(string(b), qt.Equals, "Hugo is great!")
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestFileCacheConcurrent(t *testing.T) {
|
||||
@@ -251,7 +252,6 @@ func TestFileCacheReadOrCreateErrorInRead(t *testing.T) {
|
||||
var result string
|
||||
|
||||
rf := func(failLevel int) func(info ItemInfo, r io.ReadSeeker) error {
|
||||
|
||||
return func(info ItemInfo, r io.ReadSeeker) error {
|
||||
if failLevel > 0 {
|
||||
if failLevel > 1 {
|
||||
@@ -314,12 +314,13 @@ func initConfig(fs afero.Fs, cfg config.Provider) error {
|
||||
if !filepath.IsAbs(themesDir) {
|
||||
themesDir = filepath.Join(workingDir, themesDir)
|
||||
}
|
||||
globAll := glob.MustCompile("**", '/')
|
||||
modulesClient := modules.NewClient(modules.ClientConfig{
|
||||
Fs: fs,
|
||||
WorkingDir: workingDir,
|
||||
ThemesDir: themesDir,
|
||||
ModuleConfig: modConfig,
|
||||
IgnoreVendor: true,
|
||||
IgnoreVendor: globAll,
|
||||
})
|
||||
|
||||
moduleConfig, err := modulesClient.Collect()
|
||||
@@ -344,5 +345,4 @@ func newPathsSpec(t *testing.T, fs afero.Fs, configStr string) *helpers.PathSpec
|
||||
p, err := helpers.NewPathSpec(hugofs.NewFrom(fs, cfg), cfg, nil)
|
||||
c.Assert(err, qt.IsNil)
|
||||
return p
|
||||
|
||||
}
|
||||
|
||||
Vendored
+1
-2
@@ -49,13 +49,12 @@ func (c *Cache) Clear() {
|
||||
|
||||
c.cache = make(map[string]cacheEntry)
|
||||
c.nlocker = locker.NewLocker()
|
||||
|
||||
}
|
||||
|
||||
// GetOrCreate tries to get the value with the given cache key, if not found
|
||||
// create will be called and cached.
|
||||
// This method is thread safe. It also guarantees that the create func for a given
|
||||
// key is invoced only once for this cache.
|
||||
// key is invoked only once for this cache.
|
||||
func (c *Cache) GetOrCreate(key string, create func() (interface{}, error)) (interface{}, error) {
|
||||
c.mu.RLock()
|
||||
entry, found := c.cache[key]
|
||||
|
||||
Vendored
-99
@@ -1,99 +0,0 @@
|
||||
// Copyright 2017-present The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Partition represents a cache partition where Load is the callback
|
||||
// for when the partition is needed.
|
||||
type Partition struct {
|
||||
Key string
|
||||
Load func() (map[string]interface{}, error)
|
||||
}
|
||||
|
||||
// Lazy represents a lazily loaded cache.
|
||||
type Lazy struct {
|
||||
initSync sync.Once
|
||||
initErr error
|
||||
cache map[string]interface{}
|
||||
load func() (map[string]interface{}, error)
|
||||
}
|
||||
|
||||
// NewLazy creates a lazy cache with the given load func.
|
||||
func NewLazy(load func() (map[string]interface{}, error)) *Lazy {
|
||||
return &Lazy{load: load}
|
||||
}
|
||||
|
||||
func (l *Lazy) init() error {
|
||||
l.initSync.Do(func() {
|
||||
c, err := l.load()
|
||||
l.cache = c
|
||||
l.initErr = err
|
||||
|
||||
})
|
||||
|
||||
return l.initErr
|
||||
}
|
||||
|
||||
// Get initializes the cache if not already initialized, then looks up the
|
||||
// given key.
|
||||
func (l *Lazy) Get(key string) (interface{}, bool, error) {
|
||||
l.init()
|
||||
if l.initErr != nil {
|
||||
return nil, false, l.initErr
|
||||
}
|
||||
v, found := l.cache[key]
|
||||
return v, found, nil
|
||||
}
|
||||
|
||||
// PartitionedLazyCache is a lazily loaded cache paritioned by a supplied string key.
|
||||
type PartitionedLazyCache struct {
|
||||
partitions map[string]*Lazy
|
||||
}
|
||||
|
||||
// NewPartitionedLazyCache creates a new NewPartitionedLazyCache with the supplied
|
||||
// partitions.
|
||||
func NewPartitionedLazyCache(partitions ...Partition) *PartitionedLazyCache {
|
||||
lazyPartitions := make(map[string]*Lazy, len(partitions))
|
||||
for _, partition := range partitions {
|
||||
lazyPartitions[partition.Key] = NewLazy(partition.Load)
|
||||
}
|
||||
cache := &PartitionedLazyCache{partitions: lazyPartitions}
|
||||
|
||||
return cache
|
||||
}
|
||||
|
||||
// Get initializes the partition if not already done so, then looks up the given
|
||||
// key in the given partition, returns nil if no value found.
|
||||
func (c *PartitionedLazyCache) Get(partition, key string) (interface{}, error) {
|
||||
p, found := c.partitions[partition]
|
||||
|
||||
if !found {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
v, found, err := p.Get(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if found {
|
||||
return v, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
|
||||
}
|
||||
Vendored
-138
@@ -1,138 +0,0 @@
|
||||
// Copyright 2017-present The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestNewPartitionedLazyCache(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := qt.New(t)
|
||||
|
||||
p1 := Partition{
|
||||
Key: "p1",
|
||||
Load: func() (map[string]interface{}, error) {
|
||||
return map[string]interface{}{
|
||||
"p1_1": "p1v1",
|
||||
"p1_2": "p1v2",
|
||||
"p1_nil": nil,
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
p2 := Partition{
|
||||
Key: "p2",
|
||||
Load: func() (map[string]interface{}, error) {
|
||||
return map[string]interface{}{
|
||||
"p2_1": "p2v1",
|
||||
"p2_2": "p2v2",
|
||||
"p2_3": "p2v3",
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
cache := NewPartitionedLazyCache(p1, p2)
|
||||
|
||||
v, err := cache.Get("p1", "p1_1")
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(v, qt.Equals, "p1v1")
|
||||
|
||||
v, err = cache.Get("p1", "p2_1")
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(v, qt.IsNil)
|
||||
|
||||
v, err = cache.Get("p1", "p1_nil")
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(v, qt.IsNil)
|
||||
|
||||
v, err = cache.Get("p2", "p2_3")
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(v, qt.Equals, "p2v3")
|
||||
|
||||
v, err = cache.Get("doesnotexist", "p1_1")
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(v, qt.IsNil)
|
||||
|
||||
v, err = cache.Get("p1", "doesnotexist")
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(v, qt.IsNil)
|
||||
|
||||
errorP := Partition{
|
||||
Key: "p3",
|
||||
Load: func() (map[string]interface{}, error) {
|
||||
return nil, errors.New("Failed")
|
||||
},
|
||||
}
|
||||
|
||||
cache = NewPartitionedLazyCache(errorP)
|
||||
|
||||
v, err = cache.Get("p1", "doesnotexist")
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(v, qt.IsNil)
|
||||
|
||||
_, err = cache.Get("p3", "doesnotexist")
|
||||
c.Assert(err, qt.Not(qt.IsNil))
|
||||
|
||||
}
|
||||
|
||||
func TestConcurrentPartitionedLazyCache(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := qt.New(t)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
p1 := Partition{
|
||||
Key: "p1",
|
||||
Load: func() (map[string]interface{}, error) {
|
||||
return map[string]interface{}{
|
||||
"p1_1": "p1v1",
|
||||
"p1_2": "p1v2",
|
||||
"p1_nil": nil,
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
p2 := Partition{
|
||||
Key: "p2",
|
||||
Load: func() (map[string]interface{}, error) {
|
||||
return map[string]interface{}{
|
||||
"p2_1": "p2v1",
|
||||
"p2_2": "p2v2",
|
||||
"p2_3": "p2v3",
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
cache := NewPartitionedLazyCache(p1, p2)
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for j := 0; j < 10; j++ {
|
||||
v, err := cache.Get("p1", "p1_1")
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(v, qt.Equals, "p1v1")
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
+3
-15
@@ -58,7 +58,7 @@ func (c *Inspector) MethodsFromTypes(include []reflect.Type, exclude []reflect.T
|
||||
|
||||
var methods Methods
|
||||
|
||||
var excludes = make(map[string]bool)
|
||||
excludes := make(map[string]bool)
|
||||
|
||||
if len(exclude) > 0 {
|
||||
for _, m := range c.MethodsFromTypes(exclude, nil) {
|
||||
@@ -99,11 +99,9 @@ func (c *Inspector) MethodsFromTypes(include []reflect.Type, exclude []reflect.T
|
||||
name = pkgPrefix + name
|
||||
|
||||
return name, pkg
|
||||
|
||||
}
|
||||
|
||||
for _, t := range include {
|
||||
|
||||
for i := 0; i < t.NumMethod(); i++ {
|
||||
|
||||
m := t.Method(i)
|
||||
@@ -153,7 +151,6 @@ func (c *Inspector) MethodsFromTypes(include []reflect.Type, exclude []reflect.T
|
||||
|
||||
methods = append(methods, method)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
sort.SliceStable(methods, func(i, j int) bool {
|
||||
@@ -167,16 +164,13 @@ func (c *Inspector) MethodsFromTypes(include []reflect.Type, exclude []reflect.T
|
||||
}
|
||||
|
||||
return wi < wj
|
||||
|
||||
})
|
||||
|
||||
return methods
|
||||
|
||||
}
|
||||
|
||||
func (c *Inspector) parseSource() {
|
||||
c.init.Do(func() {
|
||||
|
||||
if !strings.Contains(c.ProjectRootDir, "hugo") {
|
||||
panic("dir must be set to the Hugo root")
|
||||
}
|
||||
@@ -200,7 +194,6 @@ func (c *Inspector) parseSource() {
|
||||
filenames = append(filenames, path)
|
||||
|
||||
return nil
|
||||
|
||||
})
|
||||
|
||||
for _, filename := range filenames {
|
||||
@@ -230,7 +223,6 @@ func (c *Inspector) parseSource() {
|
||||
c.methodWeight[iface] = weights
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return true
|
||||
})
|
||||
@@ -247,7 +239,6 @@ func (c *Inspector) parseSource() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
@@ -374,7 +365,7 @@ func (m Methods) Imports() []string {
|
||||
}
|
||||
|
||||
// ToMarshalJSON creates a MarshalJSON method for these methods. Any method name
|
||||
// matchin any of the regexps in excludes will be ignored.
|
||||
// matching any of the regexps in excludes will be ignored.
|
||||
func (m Methods) ToMarshalJSON(receiver, pkgPath string, excludes ...string) (string, []string) {
|
||||
var sb strings.Builder
|
||||
|
||||
@@ -385,7 +376,7 @@ func (m Methods) ToMarshalJSON(receiver, pkgPath string, excludes ...string) (st
|
||||
fmt.Fprintf(&sb, "func Marshal%sToJSON(%s %s) ([]byte, error) {\n", what, r, receiver)
|
||||
|
||||
var methods Methods
|
||||
var excludeRes = make([]*regexp.Regexp, len(excludes))
|
||||
excludeRes := make([]*regexp.Regexp, len(excludes))
|
||||
|
||||
for i, exclude := range excludes {
|
||||
excludeRes[i] = regexp.MustCompile(exclude)
|
||||
@@ -450,7 +441,6 @@ func (m Methods) ToMarshalJSON(receiver, pkgPath string, excludes ...string) (st
|
||||
}
|
||||
|
||||
return sb.String(), pkgImports
|
||||
|
||||
}
|
||||
|
||||
func collectMethodsRecursive(pkg string, f []*ast.Field) []string {
|
||||
@@ -481,7 +471,6 @@ func collectMethodsRecursive(pkg string, f []*ast.Field) []string {
|
||||
}
|
||||
|
||||
return methodNames
|
||||
|
||||
}
|
||||
|
||||
func firstToLower(name string) string {
|
||||
@@ -544,5 +533,4 @@ func varName(name string) string {
|
||||
}
|
||||
|
||||
return name
|
||||
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ import (
|
||||
)
|
||||
|
||||
func TestMethods(t *testing.T) {
|
||||
|
||||
var (
|
||||
zeroIE = reflect.TypeOf((*IEmbed)(nil)).Elem()
|
||||
zeroIEOnly = reflect.TypeOf((*IEOnly)(nil)).Elem()
|
||||
@@ -58,7 +57,6 @@ func TestMethods(t *testing.T) {
|
||||
methodsStr := fmt.Sprint(methods)
|
||||
|
||||
c.Assert(methodsStr, qt.Contains, "MethodEmbed3(arg0 string) string")
|
||||
|
||||
})
|
||||
|
||||
t.Run("ToMarshalJSON", func(t *testing.T) {
|
||||
@@ -76,9 +74,7 @@ func TestMethods(t *testing.T) {
|
||||
c.Assert(pkg, qt.Contains, "encoding/json")
|
||||
|
||||
fmt.Println(pkg)
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
type I interface {
|
||||
|
||||
+5
-4
@@ -26,9 +26,10 @@ type checkCmd struct {
|
||||
}
|
||||
|
||||
func newCheckCmd() *checkCmd {
|
||||
return &checkCmd{baseCmd: &baseCmd{cmd: &cobra.Command{
|
||||
Use: "check",
|
||||
Short: "Contains some verification checks",
|
||||
},
|
||||
return &checkCmd{baseCmd: &baseCmd{
|
||||
cmd: &cobra.Command{
|
||||
Use: "check",
|
||||
Short: "Contains some verification checks",
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
@@ -24,10 +24,11 @@ type checkCmd struct {
|
||||
}
|
||||
|
||||
func newCheckCmd() *checkCmd {
|
||||
cc := &checkCmd{baseCmd: &baseCmd{cmd: &cobra.Command{
|
||||
Use: "check",
|
||||
Short: "Contains some verification checks",
|
||||
},
|
||||
cc := &checkCmd{baseCmd: &baseCmd{
|
||||
cmd: &cobra.Command{
|
||||
Use: "check",
|
||||
Short: "Contains some verification checks",
|
||||
},
|
||||
}}
|
||||
|
||||
cc.cmd.AddCommand(newLimitCmd().getCommand())
|
||||
|
||||
+15
-21
@@ -16,24 +16,22 @@ package commands
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
hconfig "github.com/gohugoio/hugo/config"
|
||||
|
||||
"golang.org/x/sync/semaphore"
|
||||
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/gohugoio/hugo/common/herrors"
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
|
||||
@@ -60,7 +58,7 @@ type commandeerHugoState struct {
|
||||
type commandeer struct {
|
||||
*commandeerHugoState
|
||||
|
||||
logger *loggers.Logger
|
||||
logger loggers.Logger
|
||||
serverConfig *config.Server
|
||||
|
||||
// Currently only set when in "fast render mode". But it seems to
|
||||
@@ -112,7 +110,7 @@ func (c *commandeerHugoState) hugo() *hugolib.HugoSites {
|
||||
}
|
||||
|
||||
func (c *commandeer) errCount() int {
|
||||
return int(c.logger.ErrorCounter.Count())
|
||||
return int(c.logger.LogCounters().ErrorCounter.Count())
|
||||
}
|
||||
|
||||
func (c *commandeer) getErrorWithContext() interface{} {
|
||||
@@ -156,7 +154,6 @@ func (c *commandeer) initFs(fs *hugofs.Fs) error {
|
||||
}
|
||||
|
||||
func newCommandeer(mustHaveConfigFile, running bool, h *hugoBuilderCommon, f flagsToConfigHandler, cfgInit func(c *commandeer) error, subCmdVs ...*cobra.Command) (*commandeer, error) {
|
||||
|
||||
var rebuildDebouncer func(f func())
|
||||
if running {
|
||||
// The time value used is tested with mass content replacements in a fairly big Hugo site.
|
||||
@@ -248,7 +245,6 @@ func (f *fileChangeDetector) PrepareNew() {
|
||||
}
|
||||
|
||||
func (c *commandeer) loadConfig(mustHaveConfigFile, running bool) error {
|
||||
|
||||
if c.DepsCfg == nil {
|
||||
c.DepsCfg = &deps.DepsCfg{}
|
||||
}
|
||||
@@ -277,7 +273,6 @@ func (c *commandeer) loadConfig(mustHaveConfigFile, running bool) error {
|
||||
environment := c.h.getEnvironment(running)
|
||||
|
||||
doWithConfig := func(cfg config.Provider) error {
|
||||
|
||||
if c.ftch != nil {
|
||||
c.ftch.flagsToConfig(cfg)
|
||||
}
|
||||
@@ -308,12 +303,12 @@ func (c *commandeer) loadConfig(mustHaveConfigFile, running bool) error {
|
||||
WorkingDir: dir,
|
||||
Filename: c.h.cfgFile,
|
||||
AbsConfigDir: c.h.getConfigDir(dir),
|
||||
Environ: os.Environ(),
|
||||
Environment: environment},
|
||||
Environment: environment,
|
||||
},
|
||||
cfgSetAndInit,
|
||||
doWithConfig)
|
||||
|
||||
if err != nil && mustHaveConfigFile {
|
||||
if err != nil {
|
||||
return err
|
||||
} else if mustHaveConfigFile && len(configFiles) == 0 {
|
||||
return hugolib.ErrNoConfigFile
|
||||
@@ -346,7 +341,10 @@ func (c *commandeer) loadConfig(mustHaveConfigFile, running bool) error {
|
||||
|
||||
cfg.Logger = logger
|
||||
c.logger = logger
|
||||
c.serverConfig = hconfig.DecodeServer(cfg.Cfg)
|
||||
c.serverConfig, err = hconfig.DecodeServer(cfg.Cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
createMemFs := config.GetBool("renderToMemory")
|
||||
|
||||
@@ -386,7 +384,7 @@ func (c *commandeer) loadConfig(mustHaveConfigFile, running bool) error {
|
||||
}
|
||||
|
||||
// To debug hard-to-find path issues.
|
||||
//fs.Destination = hugofs.NewStacktracerFs(fs.Destination, `fr/fr`)
|
||||
// fs.Destination = hugofs.NewStacktracerFs(fs.Destination, `fr/fr`)
|
||||
|
||||
err = c.initFs(fs)
|
||||
if err != nil {
|
||||
@@ -399,7 +397,6 @@ func (c *commandeer) loadConfig(mustHaveConfigFile, running bool) error {
|
||||
h, err = hugolib.NewHugoSites(*c.DepsCfg)
|
||||
c.hugoSites = h
|
||||
close(c.created)
|
||||
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@@ -412,8 +409,5 @@ func (c *commandeer) loadConfig(mustHaveConfigFile, running bool) error {
|
||||
}
|
||||
config.Set("cacheDir", cacheDir)
|
||||
|
||||
cfg.Logger.INFO.Println("Using config file:", config.ConfigFileUsed())
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
+18
-9
@@ -88,6 +88,7 @@ var _ commandsBuilderGetter = (*baseBuilderCmd)(nil)
|
||||
type commandsBuilderGetter interface {
|
||||
getCommandsBuilder() *commandsBuilder
|
||||
}
|
||||
|
||||
type baseBuilderCmd struct {
|
||||
*baseCmd
|
||||
*commandsBuilder
|
||||
@@ -138,7 +139,6 @@ func (c *nilCommand) getCommand() *cobra.Command {
|
||||
}
|
||||
|
||||
func (c *nilCommand) flagsToConfig(cfg config.Provider) {
|
||||
|
||||
}
|
||||
|
||||
func (b *commandsBuilder) newHugoCmd() *hugoCmd {
|
||||
@@ -162,13 +162,21 @@ Complete documentation is available at http://gohugo.io/.`,
|
||||
return nil
|
||||
}
|
||||
|
||||
// prevent cobra printing error so it can be handled here (before the timeTrack prints)
|
||||
cmd.SilenceErrors = true
|
||||
|
||||
c, err := initializeConfig(true, cc.buildWatch, &cc.hugoBuilderCommon, cc, cfgInit)
|
||||
if err != nil {
|
||||
cmd.PrintErrln("Error:", err.Error())
|
||||
return err
|
||||
}
|
||||
cc.c = c
|
||||
|
||||
return c.build()
|
||||
err = c.build()
|
||||
if err != nil {
|
||||
cmd.PrintErrln("Error:", err.Error())
|
||||
}
|
||||
return err
|
||||
},
|
||||
})
|
||||
|
||||
@@ -204,6 +212,7 @@ type hugoBuilderCommon struct {
|
||||
environment string
|
||||
|
||||
buildWatch bool
|
||||
poll string
|
||||
|
||||
gc bool
|
||||
|
||||
@@ -212,6 +221,7 @@ type hugoBuilderCommon struct {
|
||||
memprofile string
|
||||
mutexprofile string
|
||||
traceprofile string
|
||||
printm bool
|
||||
|
||||
// TODO(bep) var vs string
|
||||
logging bool
|
||||
@@ -272,6 +282,7 @@ func (cc *hugoBuilderCommon) handleCommonBuilderFlags(cmd *cobra.Command) {
|
||||
cmd.PersistentFlags().StringVarP(&cc.environment, "environment", "e", "", "build environment")
|
||||
cmd.PersistentFlags().StringP("themesDir", "", "", "filesystem path to themes directory")
|
||||
cmd.PersistentFlags().BoolP("ignoreVendor", "", false, "ignores any _vendor directory")
|
||||
cmd.PersistentFlags().StringP("ignoreVendorPaths", "", "", "ignores any _vendor for module paths matching the given Glob pattern")
|
||||
}
|
||||
|
||||
func (cc *hugoBuilderCommon) handleFlags(cmd *cobra.Command) {
|
||||
@@ -289,6 +300,7 @@ func (cc *hugoBuilderCommon) handleFlags(cmd *cobra.Command) {
|
||||
cmd.Flags().StringVarP(&cc.baseURL, "baseURL", "b", "", "hostname (and path) to the root, e.g. http://spf13.com/")
|
||||
cmd.Flags().Bool("enableGitInfo", false, "add Git revision, date and author info to the pages")
|
||||
cmd.Flags().BoolVar(&cc.gc, "gc", false, "enable to run some cleanup tasks (remove unused cache files) after the build")
|
||||
cmd.Flags().StringVar(&cc.poll, "poll", "", "set this to a poll interval, e.g --poll 700ms, to use a poll based approach to watch for file system changes")
|
||||
|
||||
cmd.Flags().Bool("templateMetrics", false, "display metrics about template executions")
|
||||
cmd.Flags().Bool("templateMetricsHints", false, "calculate some improvement hints when combined with --templateMetrics")
|
||||
@@ -299,6 +311,7 @@ func (cc *hugoBuilderCommon) handleFlags(cmd *cobra.Command) {
|
||||
cmd.Flags().BoolP("path-warnings", "", false, "print warnings on duplicate target paths etc.")
|
||||
cmd.Flags().StringVarP(&cc.cpuprofile, "profile-cpu", "", "", "write cpu profile to `file`")
|
||||
cmd.Flags().StringVarP(&cc.memprofile, "profile-mem", "", "", "write memory profile to `file`")
|
||||
cmd.Flags().BoolVarP(&cc.printm, "print-mem", "", false, "print memory usage to screen at intervals")
|
||||
cmd.Flags().StringVarP(&cc.mutexprofile, "profile-mutex", "", "", "write Mutex profile to `file`")
|
||||
cmd.Flags().StringVarP(&cc.traceprofile, "trace", "", "", "write trace to `file` (not useful in general)")
|
||||
|
||||
@@ -319,16 +332,12 @@ func (cc *hugoBuilderCommon) handleFlags(cmd *cobra.Command) {
|
||||
_ = cmd.Flags().SetAnnotation("theme", cobra.BashCompSubdirsInDir, []string{"themes"})
|
||||
}
|
||||
|
||||
func checkErr(logger *loggers.Logger, err error, s ...string) {
|
||||
func checkErr(logger loggers.Logger, err error, s ...string) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
if len(s) == 0 {
|
||||
logger.CRITICAL.Println(err)
|
||||
return
|
||||
}
|
||||
for _, message := range s {
|
||||
logger.ERROR.Println(message)
|
||||
logger.Errorln(message)
|
||||
}
|
||||
logger.ERROR.Println(err)
|
||||
logger.Errorln(err)
|
||||
}
|
||||
|
||||
+95
-86
@@ -20,6 +20,8 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/gohugoio/hugo/config"
|
||||
|
||||
"github.com/gohugoio/hugo/htesting"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
@@ -29,13 +31,11 @@ import (
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestExecute(t *testing.T) {
|
||||
|
||||
c := qt.New(t)
|
||||
|
||||
createSite := func(c *qt.C) (string, func()) {
|
||||
@@ -90,7 +90,7 @@ func TestExecute(t *testing.T) {
|
||||
defer clean()
|
||||
resp := Execute([]string{"deploy", "-s=" + dir, "-e=staging", "--target=mydeployment", "--dryRun"})
|
||||
c.Assert(resp.Err, qt.Not(qt.IsNil))
|
||||
c.Assert(resp.Err.Error(), qt.Contains, `no provider registered for "hugocloud"`)
|
||||
c.Assert(resp.Err.Error(), qt.Contains, `no driver registered for "hugocloud"`)
|
||||
})
|
||||
|
||||
c.Run("list", func(c *qt.C) {
|
||||
@@ -124,7 +124,6 @@ func TestExecute(t *testing.T) {
|
||||
c.Assert(config, qt.Contains, "baseURL = \"http://example.org/\"")
|
||||
checkNewSiteInited(c, siteDir)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func checkNewSiteInited(c *qt.C, basepath string) {
|
||||
@@ -151,7 +150,7 @@ func readFileFrom(c *qt.C, filename string) string {
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func TestCommandsPersistentFlags(t *testing.T) {
|
||||
func TestFlags(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
noOpRunE := func(cmd *cobra.Command, args []string) error {
|
||||
@@ -159,96 +158,108 @@ func TestCommandsPersistentFlags(t *testing.T) {
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
check func(command []cmder)
|
||||
}{{[]string{"server",
|
||||
"--config=myconfig.toml",
|
||||
"--configDir=myconfigdir",
|
||||
"--contentDir=mycontent",
|
||||
"--disableKinds=page,home",
|
||||
"--environment=testing",
|
||||
"--configDir=myconfigdir",
|
||||
"--layoutDir=mylayouts",
|
||||
"--theme=mytheme",
|
||||
"--gc",
|
||||
"--themesDir=mythemes",
|
||||
"--cleanDestinationDir",
|
||||
"--navigateToChanged",
|
||||
"--disableLiveReload",
|
||||
"--noHTTPCache",
|
||||
"--i18n-warnings",
|
||||
"--destination=/tmp/mydestination",
|
||||
"-b=https://example.com/b/",
|
||||
"--port=1366",
|
||||
"--renderToDisk",
|
||||
"--source=mysource",
|
||||
"--path-warnings",
|
||||
}, func(commands []cmder) {
|
||||
var sc *serverCmd
|
||||
for _, command := range commands {
|
||||
if b, ok := command.(commandsBuilderGetter); ok {
|
||||
v := b.getCommandsBuilder().hugoBuilderCommon
|
||||
c.Assert(v.cfgFile, qt.Equals, "myconfig.toml")
|
||||
c.Assert(v.cfgDir, qt.Equals, "myconfigdir")
|
||||
c.Assert(v.source, qt.Equals, "mysource")
|
||||
c.Assert(v.baseURL, qt.Equals, "https://example.com/b/")
|
||||
}
|
||||
check func(c *qt.C, cmd *serverCmd)
|
||||
}{
|
||||
{
|
||||
// https://github.com/gohugoio/hugo/issues/7642
|
||||
name: "ignoreVendor as bool",
|
||||
args: []string{"server", "--ignoreVendor"},
|
||||
check: func(c *qt.C, cmd *serverCmd) {
|
||||
cfg := config.New()
|
||||
cmd.flagsToConfig(cfg)
|
||||
c.Assert(cfg.Get("ignoreVendor"), qt.Equals, true)
|
||||
},
|
||||
},
|
||||
{
|
||||
// https://github.com/gohugoio/hugo/issues/7642
|
||||
name: "ignoreVendorPaths",
|
||||
args: []string{"server", "--ignoreVendorPaths=github.com/**"},
|
||||
check: func(c *qt.C, cmd *serverCmd) {
|
||||
cfg := config.New()
|
||||
cmd.flagsToConfig(cfg)
|
||||
c.Assert(cfg.Get("ignoreVendorPaths"), qt.Equals, "github.com/**")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Persistent flags",
|
||||
args: []string{
|
||||
"server",
|
||||
"--config=myconfig.toml",
|
||||
"--configDir=myconfigdir",
|
||||
"--contentDir=mycontent",
|
||||
"--disableKinds=page,home",
|
||||
"--environment=testing",
|
||||
"--configDir=myconfigdir",
|
||||
"--layoutDir=mylayouts",
|
||||
"--theme=mytheme",
|
||||
"--gc",
|
||||
"--themesDir=mythemes",
|
||||
"--cleanDestinationDir",
|
||||
"--navigateToChanged",
|
||||
"--disableLiveReload",
|
||||
"--noHTTPCache",
|
||||
"--i18n-warnings",
|
||||
"--destination=/tmp/mydestination",
|
||||
"-b=https://example.com/b/",
|
||||
"--port=1366",
|
||||
"--renderToDisk",
|
||||
"--source=mysource",
|
||||
"--path-warnings",
|
||||
},
|
||||
check: func(c *qt.C, sc *serverCmd) {
|
||||
c.Assert(sc, qt.Not(qt.IsNil))
|
||||
c.Assert(sc.navigateToChanged, qt.Equals, true)
|
||||
c.Assert(sc.disableLiveReload, qt.Equals, true)
|
||||
c.Assert(sc.noHTTPCache, qt.Equals, true)
|
||||
c.Assert(sc.renderToDisk, qt.Equals, true)
|
||||
c.Assert(sc.serverPort, qt.Equals, 1366)
|
||||
c.Assert(sc.environment, qt.Equals, "testing")
|
||||
|
||||
if srvCmd, ok := command.(*serverCmd); ok {
|
||||
sc = srvCmd
|
||||
}
|
||||
}
|
||||
cfg := config.New()
|
||||
sc.flagsToConfig(cfg)
|
||||
c.Assert(cfg.GetString("publishDir"), qt.Equals, "/tmp/mydestination")
|
||||
c.Assert(cfg.GetString("contentDir"), qt.Equals, "mycontent")
|
||||
c.Assert(cfg.GetString("layoutDir"), qt.Equals, "mylayouts")
|
||||
c.Assert(cfg.GetStringSlice("theme"), qt.DeepEquals, []string{"mytheme"})
|
||||
c.Assert(cfg.GetString("themesDir"), qt.Equals, "mythemes")
|
||||
c.Assert(cfg.GetString("baseURL"), qt.Equals, "https://example.com/b/")
|
||||
|
||||
c.Assert(sc, qt.Not(qt.IsNil))
|
||||
c.Assert(sc.navigateToChanged, qt.Equals, true)
|
||||
c.Assert(sc.disableLiveReload, qt.Equals, true)
|
||||
c.Assert(sc.noHTTPCache, qt.Equals, true)
|
||||
c.Assert(sc.renderToDisk, qt.Equals, true)
|
||||
c.Assert(sc.serverPort, qt.Equals, 1366)
|
||||
c.Assert(sc.environment, qt.Equals, "testing")
|
||||
c.Assert(cfg.Get("disableKinds"), qt.DeepEquals, []string{"page", "home"})
|
||||
|
||||
cfg := viper.New()
|
||||
sc.flagsToConfig(cfg)
|
||||
c.Assert(cfg.GetString("publishDir"), qt.Equals, "/tmp/mydestination")
|
||||
c.Assert(cfg.GetString("contentDir"), qt.Equals, "mycontent")
|
||||
c.Assert(cfg.GetString("layoutDir"), qt.Equals, "mylayouts")
|
||||
c.Assert(cfg.GetStringSlice("theme"), qt.DeepEquals, []string{"mytheme"})
|
||||
c.Assert(cfg.GetString("themesDir"), qt.Equals, "mythemes")
|
||||
c.Assert(cfg.GetString("baseURL"), qt.Equals, "https://example.com/b/")
|
||||
c.Assert(cfg.GetBool("gc"), qt.Equals, true)
|
||||
|
||||
c.Assert(cfg.Get("disableKinds"), qt.DeepEquals, []string{"page", "home"})
|
||||
// The flag is named path-warnings
|
||||
c.Assert(cfg.GetBool("logPathWarnings"), qt.Equals, true)
|
||||
|
||||
c.Assert(cfg.GetBool("gc"), qt.Equals, true)
|
||||
|
||||
// The flag is named path-warnings
|
||||
c.Assert(cfg.GetBool("logPathWarnings"), qt.Equals, true)
|
||||
|
||||
// The flag is named i18n-warnings
|
||||
c.Assert(cfg.GetBool("logI18nWarnings"), qt.Equals, true)
|
||||
|
||||
}}}
|
||||
|
||||
for _, test := range tests {
|
||||
b := newCommandsBuilder()
|
||||
root := b.addAll().build()
|
||||
|
||||
for _, c := range b.commands {
|
||||
if c.getCommand() == nil {
|
||||
continue
|
||||
}
|
||||
// We are only intereseted in the flag handling here.
|
||||
c.getCommand().RunE = noOpRunE
|
||||
}
|
||||
rootCmd := root.getCommand()
|
||||
rootCmd.SetArgs(test.args)
|
||||
c.Assert(rootCmd.Execute(), qt.IsNil)
|
||||
test.check(b.commands)
|
||||
// The flag is named i18n-warnings
|
||||
c.Assert(cfg.GetBool("logI18nWarnings"), qt.Equals, true)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
c.Run(test.name, func(c *qt.C) {
|
||||
b := newCommandsBuilder()
|
||||
root := b.addAll().build()
|
||||
|
||||
for _, cmd := range b.commands {
|
||||
if cmd.getCommand() == nil {
|
||||
continue
|
||||
}
|
||||
// We are only intereseted in the flag handling here.
|
||||
cmd.getCommand().RunE = noOpRunE
|
||||
}
|
||||
rootCmd := root.getCommand()
|
||||
rootCmd.SetArgs(test.args)
|
||||
c.Assert(rootCmd.Execute(), qt.IsNil)
|
||||
test.check(c, b.commands[0].(*serverCmd))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandsExecute(t *testing.T) {
|
||||
|
||||
c := qt.New(t)
|
||||
|
||||
dir, clean, err := createSimpleTestSite(t, testSiteConfig{})
|
||||
@@ -317,7 +328,6 @@ func TestCommandsExecute(t *testing.T) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type testSiteConfig struct {
|
||||
@@ -386,7 +396,6 @@ Environment: {{ hugo.Environment }}
|
||||
`)
|
||||
|
||||
return d, clean, nil
|
||||
|
||||
}
|
||||
|
||||
func writeFile(t *testing.T, filename, content string) {
|
||||
|
||||
+49
-10
@@ -21,6 +21,9 @@ import (
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
|
||||
"github.com/gohugoio/hugo/parser"
|
||||
"github.com/gohugoio/hugo/parser/metadecoders"
|
||||
@@ -28,7 +31,6 @@ import (
|
||||
"github.com/gohugoio/hugo/modules"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var _ cmder = (*configCmd)(nil)
|
||||
@@ -68,7 +70,7 @@ func (c *configCmd) printMounts(cmd *cobra.Command, args []string) error {
|
||||
allModules := cfg.Cfg.Get("allmodules").(modules.Modules)
|
||||
|
||||
for _, m := range allModules {
|
||||
if err := parser.InterfaceToConfig(&modMounts{m: m}, metadecoders.JSON, os.Stdout); err != nil {
|
||||
if err := parser.InterfaceToConfig(&modMounts{m: m, verbose: c.verbose}, metadecoders.JSON, os.Stdout); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -81,7 +83,7 @@ func (c *configCmd) printConfig(cmd *cobra.Command, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
allSettings := cfg.Cfg.(*viper.Viper).AllSettings()
|
||||
allSettings := cfg.Cfg.Get("").(maps.Params)
|
||||
|
||||
// We need to clean up this, but we store objects in the config that
|
||||
// isn't really interesting to the end user, so filter these.
|
||||
@@ -114,7 +116,8 @@ func (c *configCmd) printConfig(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
type modMounts struct {
|
||||
m modules.Module
|
||||
verbose bool
|
||||
m modules.Module
|
||||
}
|
||||
|
||||
type modMount struct {
|
||||
@@ -134,13 +137,49 @@ func (m *modMounts) MarshalJSON() ([]byte, error) {
|
||||
})
|
||||
}
|
||||
|
||||
var ownerPath string
|
||||
if m.m.Owner() != nil {
|
||||
ownerPath = m.m.Owner().Path()
|
||||
}
|
||||
|
||||
if m.verbose {
|
||||
config := m.m.Config()
|
||||
return json.Marshal(&struct {
|
||||
Path string `json:"path"`
|
||||
Version string `json:"version"`
|
||||
Time time.Time `json:"time"`
|
||||
Owner string `json:"owner"`
|
||||
Dir string `json:"dir"`
|
||||
Meta map[string]interface{} `json:"meta"`
|
||||
HugoVersion modules.HugoVersion `json:"hugoVersion"`
|
||||
|
||||
Mounts []modMount `json:"mounts"`
|
||||
}{
|
||||
Path: m.m.Path(),
|
||||
Version: m.m.Version(),
|
||||
Time: m.m.Time(),
|
||||
Owner: ownerPath,
|
||||
Dir: m.m.Dir(),
|
||||
Meta: config.Params,
|
||||
HugoVersion: config.HugoVersion,
|
||||
Mounts: mounts,
|
||||
})
|
||||
}
|
||||
|
||||
return json.Marshal(&struct {
|
||||
Path string `json:"path"`
|
||||
Dir string `json:"dir"`
|
||||
Mounts []modMount `json:"mounts"`
|
||||
Path string `json:"path"`
|
||||
Version string `json:"version"`
|
||||
Time time.Time `json:"time"`
|
||||
Owner string `json:"owner"`
|
||||
Dir string `json:"dir"`
|
||||
Mounts []modMount `json:"mounts"`
|
||||
}{
|
||||
Path: m.m.Path(),
|
||||
Dir: m.m.Dir(),
|
||||
Mounts: mounts,
|
||||
Path: m.m.Path(),
|
||||
Version: m.m.Version(),
|
||||
Time: m.m.Time(),
|
||||
Owner: ownerPath,
|
||||
Dir: m.m.Dir(),
|
||||
Mounts: mounts,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
+7
-10
@@ -16,6 +16,7 @@ package commands
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -34,14 +35,10 @@ import (
|
||||
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
_ cmder = (*convertCmd)(nil)
|
||||
)
|
||||
var _ cmder = (*convertCmd)(nil)
|
||||
|
||||
type convertCmd struct {
|
||||
outputDir string
|
||||
@@ -123,7 +120,7 @@ func (cc *convertCmd) convertContents(format metadecoders.Format) error {
|
||||
|
||||
site := h.Sites[0]
|
||||
|
||||
site.Log.FEEDBACK.Println("processing", len(site.AllPages()), "content files")
|
||||
site.Log.Println("processing", len(site.AllPages()), "content files")
|
||||
for _, p := range site.AllPages() {
|
||||
if err := cc.convertAndSavePage(p, site, format); err != nil {
|
||||
return err
|
||||
@@ -147,19 +144,19 @@ func (cc *convertCmd) convertAndSavePage(p page.Page, site *hugolib.Site, target
|
||||
|
||||
errMsg := fmt.Errorf("Error processing file %q", p.Path())
|
||||
|
||||
site.Log.INFO.Println("Attempting to convert", p.File().Filename())
|
||||
site.Log.Infoln("Attempting to convert", p.File().Filename())
|
||||
|
||||
f := p.File()
|
||||
file, err := f.FileInfo().Meta().Open()
|
||||
if err != nil {
|
||||
site.Log.ERROR.Println(errMsg)
|
||||
site.Log.Errorln(errMsg)
|
||||
file.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
pf, err := pageparser.ParseFrontMatterAndContent(file)
|
||||
if err != nil {
|
||||
site.Log.ERROR.Println(errMsg)
|
||||
site.Log.Errorln(errMsg)
|
||||
file.Close()
|
||||
return err
|
||||
}
|
||||
@@ -179,7 +176,7 @@ func (cc *convertCmd) convertAndSavePage(p page.Page, site *hugolib.Site, target
|
||||
var newContent bytes.Buffer
|
||||
err = parser.InterfaceToFrontMatter(pf.FrontMatter, targetFormat, &newContent)
|
||||
if err != nil {
|
||||
site.Log.ERROR.Println(errMsg)
|
||||
site.Log.Errorln(errMsg)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
+9
-2
@@ -11,6 +11,8 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// +build !nodeploy
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
@@ -25,6 +27,9 @@ var _ cmder = (*deployCmd)(nil)
|
||||
// deployCmd supports deploying sites to Cloud providers.
|
||||
type deployCmd struct {
|
||||
*baseBuilderCmd
|
||||
|
||||
invalidateCDN bool
|
||||
maxDeletes int
|
||||
}
|
||||
|
||||
// TODO: In addition to the "deploy" command, consider adding a "--deploy"
|
||||
@@ -51,6 +56,8 @@ documentation.
|
||||
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfgInit := func(c *commandeer) error {
|
||||
c.Set("invalidateCDN", cc.invalidateCDN)
|
||||
c.Set("maxDeletes", cc.maxDeletes)
|
||||
return nil
|
||||
}
|
||||
comm, err := initializeConfig(true, false, &cc.hugoBuilderCommon, cc, cfgInit)
|
||||
@@ -69,8 +76,8 @@ documentation.
|
||||
cmd.Flags().Bool("confirm", false, "ask for confirmation before making changes to the target")
|
||||
cmd.Flags().Bool("dryRun", false, "dry run")
|
||||
cmd.Flags().Bool("force", false, "force upload of all files")
|
||||
cmd.Flags().Bool("invalidateCDN", true, "invalidate the CDN cache listed in the deployment target")
|
||||
cmd.Flags().Int("maxDeletes", 256, "maximum # of files to delete, or -1 to disable")
|
||||
cmd.Flags().BoolVar(&cc.invalidateCDN, "invalidateCDN", true, "invalidate the CDN cache listed in the deployment target")
|
||||
cmd.Flags().IntVar(&cc.maxDeletes, "maxDeletes", 256, "maximum # of files to delete, or -1 to disable")
|
||||
|
||||
cc.baseBuilderCmd = b.newBuilderBasicCmd(cmd)
|
||||
|
||||
|
||||
+28
-12
@@ -16,6 +16,8 @@ package commands
|
||||
import (
|
||||
"runtime"
|
||||
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
)
|
||||
@@ -27,18 +29,32 @@ type envCmd struct {
|
||||
}
|
||||
|
||||
func newEnvCmd() *envCmd {
|
||||
return &envCmd{baseCmd: newBaseCmd(&cobra.Command{
|
||||
Use: "env",
|
||||
Short: "Print Hugo version and environment info",
|
||||
Long: `Print Hugo version and environment info. This is useful in Hugo bug reports.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
printHugoVersion()
|
||||
jww.FEEDBACK.Printf("GOOS=%q\n", runtime.GOOS)
|
||||
jww.FEEDBACK.Printf("GOARCH=%q\n", runtime.GOARCH)
|
||||
jww.FEEDBACK.Printf("GOVERSION=%q\n", runtime.Version())
|
||||
return &envCmd{
|
||||
baseCmd: newBaseCmd(&cobra.Command{
|
||||
Use: "env",
|
||||
Short: "Print Hugo version and environment info",
|
||||
Long: `Print Hugo version and environment info. This is useful in Hugo bug reports.
|
||||
|
||||
return nil
|
||||
},
|
||||
}),
|
||||
If you add the -v flag, you will get a full dependency list.
|
||||
`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
printHugoVersion()
|
||||
jww.FEEDBACK.Printf("GOOS=%q\n", runtime.GOOS)
|
||||
jww.FEEDBACK.Printf("GOARCH=%q\n", runtime.GOARCH)
|
||||
jww.FEEDBACK.Printf("GOVERSION=%q\n", runtime.Version())
|
||||
|
||||
isVerbose, _ := cmd.Flags().GetBool("verbose")
|
||||
|
||||
if isVerbose {
|
||||
deps := hugo.GetDependencyList()
|
||||
for _, dep := range deps {
|
||||
jww.FEEDBACK.Printf("%s\n", dep)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+33
-20
@@ -14,6 +14,9 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
)
|
||||
@@ -23,7 +26,7 @@ var _ cmder = (*genautocompleteCmd)(nil)
|
||||
type genautocompleteCmd struct {
|
||||
autocompleteTarget string
|
||||
|
||||
// bash for now (zsh and others will come)
|
||||
// bash, zsh, fish or powershell
|
||||
autocompleteType string
|
||||
|
||||
*baseCmd
|
||||
@@ -37,44 +40,54 @@ func newGenautocompleteCmd() *genautocompleteCmd {
|
||||
Short: "Generate shell autocompletion script for Hugo",
|
||||
Long: `Generates a shell autocompletion script for Hugo.
|
||||
|
||||
NOTE: The current version supports Bash only.
|
||||
This should work for *nix systems with Bash installed.
|
||||
The script is written to the console (stdout).
|
||||
|
||||
By default, the file is written directly to /etc/bash_completion.d
|
||||
for convenience, and the command may need superuser rights, e.g.:
|
||||
To write to file, add the ` + "`--completionfile=/path/to/file`" + ` flag.
|
||||
|
||||
$ sudo hugo gen autocomplete
|
||||
|
||||
Add ` + "`--completionfile=/path/to/file`" + ` flag to set alternative
|
||||
file-path and name.
|
||||
Add ` + "`--type={bash, zsh, fish or powershell}`" + ` flag to set alternative
|
||||
shell type.
|
||||
|
||||
Logout and in again to reload the completion scripts,
|
||||
or just source them in directly:
|
||||
|
||||
$ . /etc/bash_completion`,
|
||||
$ . /etc/bash_completion or /path/to/file`,
|
||||
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if cc.autocompleteType != "bash" {
|
||||
return newUserError("Only Bash is supported for now")
|
||||
var err error
|
||||
var target io.Writer
|
||||
|
||||
if cc.autocompleteTarget == "" {
|
||||
target = os.Stdout
|
||||
} else {
|
||||
target, _ = os.OpenFile(cc.autocompleteTarget, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
}
|
||||
|
||||
err := cmd.Root().GenBashCompletionFile(cc.autocompleteTarget)
|
||||
switch cc.autocompleteType {
|
||||
case "bash":
|
||||
err = cmd.Root().GenBashCompletion(target)
|
||||
case "zsh":
|
||||
err = cmd.Root().GenZshCompletion(target)
|
||||
case "fish":
|
||||
err = cmd.Root().GenFishCompletion(target, true)
|
||||
case "powershell":
|
||||
err = cmd.Root().GenPowerShellCompletion(target)
|
||||
default:
|
||||
return newUserError("Unsupported completion type")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
jww.FEEDBACK.Println("Bash completion file for Hugo saved to", cc.autocompleteTarget)
|
||||
|
||||
if cc.autocompleteTarget != "" {
|
||||
jww.FEEDBACK.Println(cc.autocompleteType+" completion file for Hugo saved to", cc.autocompleteTarget)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
cc.cmd.PersistentFlags().StringVarP(&cc.autocompleteTarget, "completionfile", "", "/etc/bash_completion.d/hugo.sh", "autocompletion file")
|
||||
cc.cmd.PersistentFlags().StringVarP(&cc.autocompleteType, "type", "", "bash", "autocompletion type (currently only bash supported)")
|
||||
|
||||
// For bash-completion
|
||||
cc.cmd.PersistentFlags().SetAnnotation("completionfile", cobra.BashCompFilenameExt, []string{})
|
||||
cc.cmd.PersistentFlags().StringVarP(&cc.autocompleteTarget, "completionfile", "f", "", "autocompletion file, defaults to stdout")
|
||||
cc.cmd.PersistentFlags().StringVarP(&cc.autocompleteType, "type", "t", "bash", "autocompletion type (bash, zsh, fish, or powershell)")
|
||||
|
||||
return cc
|
||||
}
|
||||
|
||||
@@ -22,9 +22,7 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
_ cmder = (*genChromaStyles)(nil)
|
||||
)
|
||||
var _ cmder = (*genChromaStyles)(nil)
|
||||
|
||||
type genChromaStyles struct {
|
||||
style string
|
||||
|
||||
+7
-5
@@ -18,7 +18,6 @@ import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
@@ -36,7 +35,6 @@ type genDocCmd struct {
|
||||
|
||||
func newGenDocCmd() *genDocCmd {
|
||||
const gendocFrontmatterTemplate = `---
|
||||
date: %s
|
||||
title: "%s"
|
||||
slug: %s
|
||||
url: %s
|
||||
@@ -57,6 +55,12 @@ It creates one Markdown file per command with front matter suitable
|
||||
for rendering in Hugo.`,
|
||||
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cmd.VisitParents(func(c *cobra.Command) {
|
||||
// Disable the "Auto generated by spf13/cobra on DATE"
|
||||
// as it creates a lot of diffs.
|
||||
c.DisableAutoGenTag = true
|
||||
})
|
||||
|
||||
if !strings.HasSuffix(cc.gendocdir, helpers.FilePathSeparator) {
|
||||
cc.gendocdir += helpers.FilePathSeparator
|
||||
}
|
||||
@@ -66,19 +70,17 @@ for rendering in Hugo.`,
|
||||
return err
|
||||
}
|
||||
}
|
||||
now := time.Now().Format("2006-01-02")
|
||||
prepender := func(filename string) string {
|
||||
name := filepath.Base(filename)
|
||||
base := strings.TrimSuffix(name, path.Ext(name))
|
||||
url := "/commands/" + strings.ToLower(base) + "/"
|
||||
return fmt.Sprintf(gendocFrontmatterTemplate, now, strings.Replace(base, "_", " ", -1), base, url)
|
||||
return fmt.Sprintf(gendocFrontmatterTemplate, strings.Replace(base, "_", " ", -1), base, url)
|
||||
}
|
||||
|
||||
linkHandler := func(name string) string {
|
||||
base := strings.TrimSuffix(name, path.Ext(name))
|
||||
return "/commands/" + strings.ToLower(base) + "/"
|
||||
}
|
||||
|
||||
jww.FEEDBACK.Println("Generating Hugo command-line documentation in", cc.gendocdir, "...")
|
||||
doc.GenMarkdownTreeCustom(cmd.Root(), cc.gendocdir, prepender, linkHandler)
|
||||
jww.FEEDBACK.Println("Done.")
|
||||
|
||||
@@ -23,9 +23,7 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
_ cmder = (*genDocsHelper)(nil)
|
||||
)
|
||||
var _ cmder = (*genDocsHelper)(nil)
|
||||
|
||||
type genDocsHelper struct {
|
||||
target string
|
||||
@@ -70,5 +68,4 @@ func (g *genDocsHelper) generate() error {
|
||||
|
||||
fmt.Println("Done!")
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
+112
-69
@@ -19,10 +19,18 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
"runtime/trace"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
|
||||
@@ -31,21 +39,14 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/gohugoio/hugo/common/herrors"
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/common/terminal"
|
||||
|
||||
"syscall"
|
||||
|
||||
"github.com/gohugoio/hugo/hugolib/filesystems"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/config"
|
||||
|
||||
flag "github.com/spf13/pflag"
|
||||
@@ -119,17 +120,15 @@ func initializeConfig(mustHaveConfigFile, running bool,
|
||||
h *hugoBuilderCommon,
|
||||
f flagsToConfigHandler,
|
||||
cfgInit func(c *commandeer) error) (*commandeer, error) {
|
||||
|
||||
c, err := newCommandeer(mustHaveConfigFile, running, h, f, cfgInit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
|
||||
}
|
||||
|
||||
func (c *commandeer) createLogger(cfg config.Provider, running bool) (*loggers.Logger, error) {
|
||||
func (c *commandeer) createLogger(cfg config.Provider, running bool) (loggers.Logger, error) {
|
||||
var (
|
||||
logHandle = ioutil.Discard
|
||||
logThreshold = jww.LevelWarn
|
||||
@@ -199,6 +198,7 @@ func initializeFlags(cmd *cobra.Command, cfg config.Provider) {
|
||||
"noTimes",
|
||||
"noChmod",
|
||||
"ignoreVendor",
|
||||
"ignoreVendorPaths",
|
||||
"templateMetrics",
|
||||
"templateMetricsHints",
|
||||
|
||||
@@ -244,7 +244,6 @@ func initializeFlags(cmd *cobra.Command, cfg config.Provider) {
|
||||
setValueFromFlag(cmd.Flags(), "destination", cfg, "publishDir", false)
|
||||
setValueFromFlag(cmd.Flags(), "i18n-warnings", cfg, "logI18nWarnings", false)
|
||||
setValueFromFlag(cmd.Flags(), "path-warnings", cfg, "logPathWarnings", false)
|
||||
|
||||
}
|
||||
|
||||
func setValueFromFlag(flags *flag.FlagSet, key string, cfg config.Provider, targetKey string, force bool) {
|
||||
@@ -277,24 +276,17 @@ func setValueFromFlag(flags *flag.FlagSet, key string, cfg config.Provider, targ
|
||||
|
||||
func isTerminal() bool {
|
||||
return terminal.IsTerminal(os.Stdout)
|
||||
|
||||
}
|
||||
func ifTerminal(s string) string {
|
||||
if !isTerminal() {
|
||||
return ""
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (c *commandeer) fullBuild() error {
|
||||
|
||||
var (
|
||||
g errgroup.Group
|
||||
langCount map[string]uint64
|
||||
)
|
||||
|
||||
if !c.h.quiet {
|
||||
fmt.Print(ifTerminal(hideCursor) + "Building sites … ")
|
||||
fmt.Println("Start building sites … ")
|
||||
fmt.Println(hugo.BuildVersionString())
|
||||
if isTerminal() {
|
||||
defer func() {
|
||||
fmt.Print(showCursor + clearLine)
|
||||
@@ -303,7 +295,6 @@ func (c *commandeer) fullBuild() error {
|
||||
}
|
||||
|
||||
copyStaticFunc := func() error {
|
||||
|
||||
cnt, err := c.copyStatic()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Error copying static files")
|
||||
@@ -351,7 +342,6 @@ func (c *commandeer) fullBuild() error {
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (c *commandeer) initCPUProfile() (func(), error) {
|
||||
@@ -379,12 +369,12 @@ func (c *commandeer) initMemProfile() {
|
||||
|
||||
f, err := os.Create(c.h.memprofile)
|
||||
if err != nil {
|
||||
c.logger.ERROR.Println("could not create memory profile: ", err)
|
||||
c.logger.Errorf("could not create memory profile: ", err)
|
||||
}
|
||||
defer f.Close()
|
||||
runtime.GC() // get up-to-date statistics
|
||||
if err := pprof.WriteHeapProfile(f); err != nil {
|
||||
c.logger.ERROR.Println("could not write memory profile: ", err)
|
||||
c.logger.Errorf("could not write memory profile: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -424,7 +414,33 @@ func (c *commandeer) initMutexProfile() (func(), error) {
|
||||
pprof.Lookup("mutex").WriteTo(f, 0)
|
||||
f.Close()
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *commandeer) initMemTicker() func() {
|
||||
memticker := time.NewTicker(5 * time.Second)
|
||||
quit := make(chan struct{})
|
||||
printMem := func() {
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
fmt.Printf("\n\nAlloc = %v\nTotalAlloc = %v\nSys = %v\nNumGC = %v\n\n", formatByteCount(m.Alloc), formatByteCount(m.TotalAlloc), formatByteCount(m.Sys), m.NumGC)
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-memticker.C:
|
||||
printMem()
|
||||
case <-quit:
|
||||
memticker.Stop()
|
||||
printMem()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return func() {
|
||||
close(quit)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *commandeer) initProfiling() (func(), error) {
|
||||
@@ -443,6 +459,11 @@ func (c *commandeer) initProfiling() (func(), error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var stopMemTicker func()
|
||||
if c.h.printm {
|
||||
stopMemTicker = c.initMemTicker()
|
||||
}
|
||||
|
||||
return func() {
|
||||
c.initMemProfile()
|
||||
|
||||
@@ -456,6 +477,10 @@ func (c *commandeer) initProfiling() (func(), error) {
|
||||
if stopTraceProf != nil {
|
||||
stopTraceProf()
|
||||
}
|
||||
|
||||
if stopMemTicker != nil {
|
||||
stopMemTicker()
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -484,7 +509,7 @@ func (c *commandeer) build() error {
|
||||
if createCounter, ok := c.destinationFs.(hugofs.DuplicatesReporter); ok {
|
||||
dupes := createCounter.ReportDuplicates()
|
||||
if dupes != "" {
|
||||
c.logger.WARN.Println("Duplicate target paths:", dupes)
|
||||
c.logger.Warnln("Duplicate target paths:", dupes)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -498,13 +523,13 @@ func (c *commandeer) build() error {
|
||||
baseWatchDir := c.Cfg.GetString("workingDir")
|
||||
rootWatchDirs := getRootWatchDirsStr(baseWatchDir, watchDirs)
|
||||
|
||||
c.logger.FEEDBACK.Printf("Watching for changes in %s%s{%s}\n", baseWatchDir, helpers.FilePathSeparator, rootWatchDirs)
|
||||
c.logger.FEEDBACK.Println("Press Ctrl+C to stop")
|
||||
watcher, err := c.newWatcher(watchDirs...)
|
||||
c.logger.Printf("Watching for changes in %s%s{%s}\n", baseWatchDir, helpers.FilePathSeparator, rootWatchDirs)
|
||||
c.logger.Println("Press Ctrl+C to stop")
|
||||
watcher, err := c.newWatcher(c.h.poll, watchDirs...)
|
||||
checkErr(c.Logger, err)
|
||||
defer watcher.Close()
|
||||
|
||||
var sigs = make(chan os.Signal, 1)
|
||||
sigs := make(chan os.Signal, 1)
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
<-sigs
|
||||
@@ -514,7 +539,6 @@ func (c *commandeer) build() error {
|
||||
}
|
||||
|
||||
func (c *commandeer) serverBuild() error {
|
||||
defer c.timeTrack(time.Now(), "Built")
|
||||
|
||||
stopProfiling, err := c.initProfiling()
|
||||
if err != nil {
|
||||
@@ -550,13 +574,12 @@ func (c *commandeer) copyStatic() (map[string]uint64, error) {
|
||||
}
|
||||
|
||||
func (c *commandeer) doWithPublishDirs(f func(sourceFs *filesystems.SourceFilesystem) (uint64, error)) (map[string]uint64, error) {
|
||||
|
||||
langCount := make(map[string]uint64)
|
||||
|
||||
staticFilesystems := c.hugo().BaseFs.SourceFilesystems.Static
|
||||
|
||||
if len(staticFilesystems) == 0 {
|
||||
c.logger.INFO.Println("No static directories found to sync")
|
||||
c.logger.Infoln("No static directories found to sync")
|
||||
return langCount, nil
|
||||
}
|
||||
|
||||
@@ -628,13 +651,13 @@ func (c *commandeer) copyStaticTo(sourceFs *filesystems.SourceFilesystem) (uint6
|
||||
syncer.Delete = c.Cfg.GetBool("cleanDestinationDir")
|
||||
|
||||
if syncer.Delete {
|
||||
c.logger.INFO.Println("removing all files from destination that don't exist in static dirs")
|
||||
c.logger.Infoln("removing all files from destination that don't exist in static dirs")
|
||||
|
||||
syncer.DeleteFilter = func(f os.FileInfo) bool {
|
||||
return f.IsDir() && strings.HasPrefix(f.Name(), ".")
|
||||
}
|
||||
}
|
||||
c.logger.INFO.Println("syncing static files to", publishDir)
|
||||
c.logger.Infoln("syncing static files to", publishDir)
|
||||
|
||||
// because we are using a baseFs (to get the union right).
|
||||
// set sync src to root
|
||||
@@ -655,7 +678,7 @@ func (c *commandeer) firstPathSpec() *helpers.PathSpec {
|
||||
|
||||
func (c *commandeer) timeTrack(start time.Time, name string) {
|
||||
elapsed := time.Since(start)
|
||||
c.logger.FEEDBACK.Printf("%s in %v ms", name, int(1000*elapsed.Seconds()))
|
||||
c.logger.Printf("%s in %v ms", name, int(1000*elapsed.Seconds()))
|
||||
}
|
||||
|
||||
// getDirList provides NewWatcher() with a list of directories to watch for changes.
|
||||
@@ -664,7 +687,7 @@ func (c *commandeer) getDirList() ([]string, error) {
|
||||
|
||||
walkFn := func(path string, fi hugofs.FileMetaInfo, err error) error {
|
||||
if err != nil {
|
||||
c.logger.ERROR.Println("walker: ", err)
|
||||
c.logger.Errorln("walker: ", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -674,23 +697,22 @@ func (c *commandeer) getDirList() ([]string, error) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
filenames = append(filenames, fi.Meta().Filename())
|
||||
filenames = append(filenames, fi.Meta().Filename)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
watchFiles := c.hugo().PathSpec.BaseFs.WatchDirs()
|
||||
for _, fi := range watchFiles {
|
||||
if !fi.IsDir() {
|
||||
filenames = append(filenames, fi.Meta().Filename())
|
||||
filenames = append(filenames, fi.Meta().Filename)
|
||||
continue
|
||||
}
|
||||
|
||||
w := hugofs.NewWalkway(hugofs.WalkwayConfig{Logger: c.logger, Info: fi, WalkFn: walkFn})
|
||||
if err := w.Walk(); err != nil {
|
||||
c.logger.ERROR.Println("walker: ", err)
|
||||
c.logger.Errorln("walker: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -706,20 +728,18 @@ func (c *commandeer) buildSites() (err error) {
|
||||
func (c *commandeer) handleBuildErr(err error, msg string) {
|
||||
c.buildErr = err
|
||||
|
||||
c.logger.ERROR.Print(msg + ":\n\n")
|
||||
c.logger.ERROR.Println(helpers.FirstUpper(err.Error()))
|
||||
c.logger.Errorln(msg + ":\n")
|
||||
c.logger.Errorln(helpers.FirstUpper(err.Error()))
|
||||
if !c.h.quiet && c.h.verbose {
|
||||
herrors.PrintStackTraceFromErr(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *commandeer) rebuildSites(events []fsnotify.Event) error {
|
||||
defer c.timeTrack(time.Now(), "Total")
|
||||
|
||||
c.buildErr = nil
|
||||
visited := c.visitedURLs.PeekAllSet()
|
||||
if c.fastRenderMode {
|
||||
|
||||
// Make sure we always render the home pages
|
||||
for _, l := range c.languages {
|
||||
langPath := c.hugo().PathSpec.GetLangSubDir(l.Lang)
|
||||
@@ -729,7 +749,6 @@ func (c *commandeer) rebuildSites(events []fsnotify.Event) error {
|
||||
home := c.hugo().PathSpec.PrependBasePath("/"+langPath, false)
|
||||
visited[home] = true
|
||||
}
|
||||
|
||||
}
|
||||
return c.hugo().Build(hugolib.BuildCfg{RecentlyVisited: visited, ErrRecovery: c.wasError}, events...)
|
||||
}
|
||||
@@ -759,13 +778,11 @@ func (c *commandeer) fullRebuild(changeType string) {
|
||||
c.fullRebuildSem.Acquire(context.Background(), 1)
|
||||
|
||||
go func() {
|
||||
|
||||
defer c.fullRebuildSem.Release(1)
|
||||
|
||||
c.printChangeDetected(changeType)
|
||||
|
||||
defer func() {
|
||||
|
||||
// Allow any file system events to arrive back.
|
||||
// This will block any rebuild on config changes for the
|
||||
// duration of the sleep.
|
||||
@@ -788,13 +805,13 @@ func (c *commandeer) fullRebuild(changeType string) {
|
||||
if !c.paused {
|
||||
_, err := c.copyStatic()
|
||||
if err != nil {
|
||||
c.logger.ERROR.Println(err)
|
||||
c.logger.Errorln(err)
|
||||
return
|
||||
}
|
||||
|
||||
err = c.buildSites()
|
||||
if err != nil {
|
||||
c.logger.ERROR.Println(err)
|
||||
c.logger.Errorln(err)
|
||||
} else if !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload") {
|
||||
livereload.ForceRefresh()
|
||||
}
|
||||
@@ -803,7 +820,7 @@ func (c *commandeer) fullRebuild(changeType string) {
|
||||
}
|
||||
|
||||
// newWatcher creates a new watcher to watch filesystem events.
|
||||
func (c *commandeer) newWatcher(dirList ...string) (*watcher.Batcher, error) {
|
||||
func (c *commandeer) newWatcher(pollIntervalStr string, dirList ...string) (*watcher.Batcher, error) {
|
||||
if runtime.GOOS == "darwin" {
|
||||
tweakLimit()
|
||||
}
|
||||
@@ -813,8 +830,17 @@ func (c *commandeer) newWatcher(dirList ...string) (*watcher.Batcher, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
watcher, err := watcher.New(1 * time.Second)
|
||||
var pollInterval time.Duration
|
||||
poll := pollIntervalStr != ""
|
||||
if poll {
|
||||
pollInterval, err = types.ToDurationE(pollIntervalStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid value for flag poll: %s", err)
|
||||
}
|
||||
c.logger.Printf("Use watcher with poll interval %v", pollInterval)
|
||||
}
|
||||
|
||||
watcher, err := watcher.New(500*time.Millisecond, pollInterval, poll)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -828,7 +854,7 @@ func (c *commandeer) newWatcher(dirList ...string) (*watcher.Batcher, error) {
|
||||
// Identifies changes to config (config.toml) files.
|
||||
configSet := make(map[string]bool)
|
||||
|
||||
c.logger.FEEDBACK.Println("Watching for config changes in", strings.Join(c.configFiles, ", "))
|
||||
c.logger.Println("Watching for config changes in", strings.Join(c.configFiles, ", "))
|
||||
for _, configFile := range c.configFiles {
|
||||
watcher.Add(configFile)
|
||||
configSet[configFile] = true
|
||||
@@ -843,9 +869,9 @@ func (c *commandeer) newWatcher(dirList ...string) (*watcher.Batcher, error) {
|
||||
// Need to reload browser to show the error
|
||||
livereload.ForceRefresh()
|
||||
}
|
||||
case err := <-watcher.Errors:
|
||||
case err := <-watcher.Errors():
|
||||
if err != nil {
|
||||
c.logger.ERROR.Println("Error while watching:", err)
|
||||
c.logger.Errorln("Error while watching:", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -861,9 +887,9 @@ func (c *commandeer) printChangeDetected(typ string) {
|
||||
}
|
||||
msg += " detected, rebuilding site."
|
||||
|
||||
c.logger.FEEDBACK.Println(msg)
|
||||
c.logger.Println(msg)
|
||||
const layout = "2006-01-02 15:04:05.000 -0700"
|
||||
c.logger.FEEDBACK.Println(time.Now().Format(layout))
|
||||
c.logger.Println(time.Now().Format(layout))
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -875,7 +901,6 @@ func (c *commandeer) handleEvents(watcher *watcher.Batcher,
|
||||
staticSyncer *staticSyncer,
|
||||
evs []fsnotify.Event,
|
||||
configSet map[string]bool) {
|
||||
|
||||
defer func() {
|
||||
c.wasError = false
|
||||
}()
|
||||
@@ -916,7 +941,6 @@ func (c *commandeer) handleEvents(watcher *watcher.Batcher,
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Config file(s) changed. Need full rebuild.
|
||||
@@ -945,14 +969,16 @@ func (c *commandeer) handleEvents(watcher *watcher.Batcher,
|
||||
return
|
||||
}
|
||||
|
||||
c.logger.INFO.Println("Received System Events:", evs)
|
||||
c.logger.Infoln("Received System Events:", evs)
|
||||
|
||||
staticEvents := []fsnotify.Event{}
|
||||
dynamicEvents := []fsnotify.Event{}
|
||||
|
||||
// Special handling for symbolic links inside /content.
|
||||
filtered := []fsnotify.Event{}
|
||||
for _, ev := range evs {
|
||||
if c.hugo().ShouldSkipFileChangeEvent(ev) {
|
||||
continue
|
||||
}
|
||||
// Check the most specific first, i.e. files.
|
||||
contentMapped := c.hugo().ContentChanges.GetSymbolicLinkMappings(ev.Name)
|
||||
if len(contentMapped) > 0 {
|
||||
@@ -1025,7 +1051,7 @@ func (c *commandeer) handleEvents(watcher *watcher.Batcher,
|
||||
|
||||
walkAdder := func(path string, f hugofs.FileMetaInfo, err error) error {
|
||||
if f.IsDir() {
|
||||
c.logger.FEEDBACK.Println("adding created directory to watchlist", path)
|
||||
c.logger.Println("adding created directory to watchlist", path)
|
||||
if err := watcher.Add(path); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1057,15 +1083,15 @@ func (c *commandeer) handleEvents(watcher *watcher.Batcher,
|
||||
c.printChangeDetected("Static files")
|
||||
|
||||
if c.Cfg.GetBool("forceSyncStatic") {
|
||||
c.logger.FEEDBACK.Printf("Syncing all static files\n")
|
||||
c.logger.Printf("Syncing all static files\n")
|
||||
_, err := c.copyStatic()
|
||||
if err != nil {
|
||||
c.logger.ERROR.Println("Error copying static files to publish dir:", err)
|
||||
c.logger.Errorln("Error copying static files to publish dir:", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err := staticSyncer.syncsStaticEvents(staticEvents); err != nil {
|
||||
c.logger.ERROR.Println("Error syncing static files to publish dir:", err)
|
||||
c.logger.Errorln("Error syncing static files to publish dir:", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -1096,9 +1122,13 @@ func (c *commandeer) handleEvents(watcher *watcher.Batcher,
|
||||
|
||||
c.printChangeDetected("")
|
||||
c.changeDetector.PrepareNew()
|
||||
if err := c.rebuildSites(dynamicEvents); err != nil {
|
||||
c.handleBuildErr(err, "Rebuild failed")
|
||||
}
|
||||
|
||||
func() {
|
||||
defer c.timeTrack(time.Now(), "Total")
|
||||
if err := c.rebuildSites(dynamicEvents); err != nil {
|
||||
c.handleBuildErr(err, "Rebuild failed")
|
||||
}
|
||||
}()
|
||||
|
||||
if doLiveReload {
|
||||
if len(partitionedEvents.ContentEvents) == 0 && len(partitionedEvents.AssetEvents) > 0 {
|
||||
@@ -1158,7 +1188,6 @@ func partitionDynamicEvents(sourceFs *filesystems.SourceFilesystems, events []fs
|
||||
}
|
||||
}
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
func pickOneWriteOrCreatePath(events []fsnotify.Event) string {
|
||||
@@ -1175,3 +1204,17 @@ func pickOneWriteOrCreatePath(events []fsnotify.Event) string {
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
func formatByteCount(b uint64) string {
|
||||
const unit = 1000
|
||||
if b < unit {
|
||||
return fmt.Sprintf("%d B", b)
|
||||
}
|
||||
div, exp := int64(unit), 0
|
||||
for n := b / unit; n >= unit; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
return fmt.Sprintf("%.1f %cB",
|
||||
float64(b)/float64(div), "kMGTPE"[exp])
|
||||
}
|
||||
|
||||
@@ -44,5 +44,4 @@ contentDir = "thisdoesnotexist"
|
||||
|
||||
_, err = cmd.ExecuteC()
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
}
|
||||
|
||||
@@ -74,11 +74,9 @@ Import from Jekyll requires two paths, e.g. ` + "`hugo import jekyll jekyll_root
|
||||
cc.cmd.AddCommand(importJekyllCmd)
|
||||
|
||||
return cc
|
||||
|
||||
}
|
||||
|
||||
func (i *importCmd) importFromJekyll(cmd *cobra.Command, args []string) error {
|
||||
|
||||
if len(args) < 2 {
|
||||
return newUserError(`import from jekyll requires two paths, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`.")
|
||||
}
|
||||
@@ -255,13 +253,11 @@ func (i *importCmd) loadJekyllConfig(fs afero.Fs, jekyllRoot string) map[string]
|
||||
defer f.Close()
|
||||
|
||||
b, err := ioutil.ReadAll(f)
|
||||
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
c, err := metadecoders.Default.UnmarshalToMap(b, metadecoders.YAML)
|
||||
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -338,8 +334,10 @@ func (i *importCmd) copyJekyllFilesAndFolders(jekyllRoot, dest string, jekyllPos
|
||||
}
|
||||
} else {
|
||||
lowerEntryName := strings.ToLower(entry.Name())
|
||||
exceptSuffix := []string{".md", ".markdown", ".html", ".htm",
|
||||
".xml", ".textile", "rakefile", "gemfile", ".lock"}
|
||||
exceptSuffix := []string{
|
||||
".md", ".markdown", ".html", ".htm",
|
||||
".xml", ".textile", "rakefile", "gemfile", ".lock",
|
||||
}
|
||||
isExcept := false
|
||||
for _, suffix := range exceptSuffix {
|
||||
if strings.HasSuffix(lowerEntryName, suffix) {
|
||||
@@ -602,8 +600,8 @@ func replaceImageTag(match string) string {
|
||||
}
|
||||
result.WriteString(">}}")
|
||||
return result.String()
|
||||
|
||||
}
|
||||
|
||||
func replaceOptionalPart(buffer *bytes.Buffer, partName string, part string) {
|
||||
if len(part) > 0 {
|
||||
buffer.WriteString(partName + "=\"" + part + "\" ")
|
||||
|
||||
@@ -53,25 +53,41 @@ func TestConvertJekyllMetadata(t *testing.T) {
|
||||
draft bool
|
||||
expect string
|
||||
}{
|
||||
{map[interface{}]interface{}{}, "testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false,
|
||||
`{"date":"2015-10-01T00:00:00Z"}`},
|
||||
{map[interface{}]interface{}{}, "testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), true,
|
||||
`{"date":"2015-10-01T00:00:00Z","draft":true}`},
|
||||
{map[interface{}]interface{}{"Permalink": "/permalink.html", "layout": "post"},
|
||||
{
|
||||
map[interface{}]interface{}{},
|
||||
"testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false,
|
||||
`{"date":"2015-10-01T00:00:00Z","url":"/permalink.html"}`},
|
||||
{map[interface{}]interface{}{"permalink": "/permalink.html"},
|
||||
`{"date":"2015-10-01T00:00:00Z"}`,
|
||||
},
|
||||
{
|
||||
map[interface{}]interface{}{},
|
||||
"testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), true,
|
||||
`{"date":"2015-10-01T00:00:00Z","draft":true}`,
|
||||
},
|
||||
{
|
||||
map[interface{}]interface{}{"Permalink": "/permalink.html", "layout": "post"},
|
||||
"testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false,
|
||||
`{"date":"2015-10-01T00:00:00Z","url":"/permalink.html"}`},
|
||||
{map[interface{}]interface{}{"category": nil, "permalink": 123},
|
||||
`{"date":"2015-10-01T00:00:00Z","url":"/permalink.html"}`,
|
||||
},
|
||||
{
|
||||
map[interface{}]interface{}{"permalink": "/permalink.html"},
|
||||
"testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false,
|
||||
`{"date":"2015-10-01T00:00:00Z"}`},
|
||||
{map[interface{}]interface{}{"Excerpt_Separator": "sep"},
|
||||
`{"date":"2015-10-01T00:00:00Z","url":"/permalink.html"}`,
|
||||
},
|
||||
{
|
||||
map[interface{}]interface{}{"category": nil, "permalink": 123},
|
||||
"testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false,
|
||||
`{"date":"2015-10-01T00:00:00Z","excerpt_separator":"sep"}`},
|
||||
{map[interface{}]interface{}{"category": "book", "layout": "post", "Others": "Goods", "Date": "2015-10-01 12:13:11"},
|
||||
`{"date":"2015-10-01T00:00:00Z"}`,
|
||||
},
|
||||
{
|
||||
map[interface{}]interface{}{"Excerpt_Separator": "sep"},
|
||||
"testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false,
|
||||
`{"Others":"Goods","categories":["book"],"date":"2015-10-01T12:13:11Z"}`},
|
||||
`{"date":"2015-10-01T00:00:00Z","excerpt_separator":"sep"}`,
|
||||
},
|
||||
{
|
||||
map[interface{}]interface{}{"category": "book", "layout": "post", "Others": "Goods", "Date": "2015-10-01 12:13:11"},
|
||||
"testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false,
|
||||
`{"Others":"Goods","categories":["book"],"date":"2015-10-01T12:13:11Z"}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, data := range testDataList {
|
||||
@@ -90,44 +106,68 @@ func TestConvertJekyllContent(t *testing.T) {
|
||||
content string
|
||||
expect string
|
||||
}{
|
||||
{map[interface{}]interface{}{},
|
||||
"Test content\r\n<!-- more -->\npart2 content", "Test content\n<!--more-->\npart2 content"},
|
||||
{map[interface{}]interface{}{},
|
||||
"Test content\n<!-- More -->\npart2 content", "Test content\n<!--more-->\npart2 content"},
|
||||
{map[interface{}]interface{}{"excerpt_separator": "<!--sep-->"},
|
||||
{
|
||||
map[interface{}]interface{}{},
|
||||
"Test content\r\n<!-- more -->\npart2 content", "Test content\n<!--more-->\npart2 content",
|
||||
},
|
||||
{
|
||||
map[interface{}]interface{}{},
|
||||
"Test content\n<!-- More -->\npart2 content", "Test content\n<!--more-->\npart2 content",
|
||||
},
|
||||
{
|
||||
map[interface{}]interface{}{"excerpt_separator": "<!--sep-->"},
|
||||
"Test content\n<!--sep-->\npart2 content",
|
||||
"---\nexcerpt_separator: <!--sep-->\n---\nTest content\n<!--more-->\npart2 content"},
|
||||
"---\nexcerpt_separator: <!--sep-->\n---\nTest content\n<!--more-->\npart2 content",
|
||||
},
|
||||
{map[interface{}]interface{}{}, "{% raw %}text{% endraw %}", "text"},
|
||||
{map[interface{}]interface{}{}, "{%raw%} text2 {%endraw %}", "text2"},
|
||||
{map[interface{}]interface{}{},
|
||||
{
|
||||
map[interface{}]interface{}{},
|
||||
"{% highlight go %}\nvar s int\n{% endhighlight %}",
|
||||
"{{< highlight go >}}\nvar s int\n{{< / highlight >}}"},
|
||||
{map[interface{}]interface{}{},
|
||||
"{{< highlight go >}}\nvar s int\n{{< / highlight >}}",
|
||||
},
|
||||
{
|
||||
map[interface{}]interface{}{},
|
||||
"{% highlight go linenos hl_lines=\"1 2\" %}\nvar s string\nvar i int\n{% endhighlight %}",
|
||||
"{{< highlight go \"linenos=table,hl_lines=1 2\" >}}\nvar s string\nvar i int\n{{< / highlight >}}"},
|
||||
"{{< highlight go \"linenos=table,hl_lines=1 2\" >}}\nvar s string\nvar i int\n{{< / highlight >}}",
|
||||
},
|
||||
|
||||
// Octopress image tag
|
||||
{map[interface{}]interface{}{},
|
||||
{
|
||||
map[interface{}]interface{}{},
|
||||
"{% img http://placekitten.com/890/280 %}",
|
||||
"{{< figure src=\"http://placekitten.com/890/280\" >}}"},
|
||||
{map[interface{}]interface{}{},
|
||||
"{{< figure src=\"http://placekitten.com/890/280\" >}}",
|
||||
},
|
||||
{
|
||||
map[interface{}]interface{}{},
|
||||
"{% img left http://placekitten.com/320/250 Place Kitten #2 %}",
|
||||
"{{< figure class=\"left\" src=\"http://placekitten.com/320/250\" title=\"Place Kitten #2\" >}}"},
|
||||
{map[interface{}]interface{}{},
|
||||
"{{< figure class=\"left\" src=\"http://placekitten.com/320/250\" title=\"Place Kitten #2\" >}}",
|
||||
},
|
||||
{
|
||||
map[interface{}]interface{}{},
|
||||
"{% img right http://placekitten.com/300/500 150 250 'Place Kitten #3' %}",
|
||||
"{{< figure class=\"right\" src=\"http://placekitten.com/300/500\" width=\"150\" height=\"250\" title=\"Place Kitten #3\" >}}"},
|
||||
{map[interface{}]interface{}{},
|
||||
"{{< figure class=\"right\" src=\"http://placekitten.com/300/500\" width=\"150\" height=\"250\" title=\"Place Kitten #3\" >}}",
|
||||
},
|
||||
{
|
||||
map[interface{}]interface{}{},
|
||||
"{% img right http://placekitten.com/300/500 150 250 'Place Kitten #4' 'An image of a very cute kitten' %}",
|
||||
"{{< figure class=\"right\" src=\"http://placekitten.com/300/500\" width=\"150\" height=\"250\" title=\"Place Kitten #4\" alt=\"An image of a very cute kitten\" >}}"},
|
||||
{map[interface{}]interface{}{},
|
||||
"{{< figure class=\"right\" src=\"http://placekitten.com/300/500\" width=\"150\" height=\"250\" title=\"Place Kitten #4\" alt=\"An image of a very cute kitten\" >}}",
|
||||
},
|
||||
{
|
||||
map[interface{}]interface{}{},
|
||||
"{% img http://placekitten.com/300/500 150 250 'Place Kitten #4' 'An image of a very cute kitten' %}",
|
||||
"{{< figure src=\"http://placekitten.com/300/500\" width=\"150\" height=\"250\" title=\"Place Kitten #4\" alt=\"An image of a very cute kitten\" >}}"},
|
||||
{map[interface{}]interface{}{},
|
||||
"{{< figure src=\"http://placekitten.com/300/500\" width=\"150\" height=\"250\" title=\"Place Kitten #4\" alt=\"An image of a very cute kitten\" >}}",
|
||||
},
|
||||
{
|
||||
map[interface{}]interface{}{},
|
||||
"{% img right /placekitten/300/500 'Place Kitten #4' 'An image of a very cute kitten' %}",
|
||||
"{{< figure class=\"right\" src=\"/placekitten/300/500\" title=\"Place Kitten #4\" alt=\"An image of a very cute kitten\" >}}"},
|
||||
{map[interface{}]interface{}{"category": "book", "layout": "post", "Date": "2015-10-01 12:13:11"},
|
||||
"{{< figure class=\"right\" src=\"/placekitten/300/500\" title=\"Place Kitten #4\" alt=\"An image of a very cute kitten\" >}}",
|
||||
},
|
||||
{
|
||||
map[interface{}]interface{}{"category": "book", "layout": "post", "Date": "2015-10-01 12:13:11"},
|
||||
"somecontent",
|
||||
"---\nDate: \"2015-10-01 12:13:11\"\ncategory: book\nlayout: post\n---\nsomecontent"},
|
||||
"---\nDate: \"2015-10-01 12:13:11\"\ncategory: book\nlayout: post\n---\nsomecontent",
|
||||
},
|
||||
}
|
||||
for _, data := range testDataList {
|
||||
result, err := convertJekyllContent(data.metadata, data.content)
|
||||
|
||||
@@ -46,7 +46,6 @@ func (lc *listCmd) buildSites(config map[string]interface{}) (*hugolib.HugoSites
|
||||
}
|
||||
|
||||
sites, err := hugolib.NewHugoSites(*c.DepsCfg)
|
||||
|
||||
if err != nil {
|
||||
return nil, newSystemError("Error creating sites", err)
|
||||
}
|
||||
@@ -77,7 +76,6 @@ List requires a subcommand, e.g. ` + "`hugo list drafts`.",
|
||||
Long: `List all of the drafts in your content directory.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
sites, err := cc.buildSites(map[string]interface{}{"buildDrafts": true})
|
||||
|
||||
if err != nil {
|
||||
return newSystemError("Error building sites", err)
|
||||
}
|
||||
@@ -97,7 +95,6 @@ List requires a subcommand, e.g. ` + "`hugo list drafts`.",
|
||||
Long: `List all of the posts in your content directory which will be posted in the future.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
sites, err := cc.buildSites(map[string]interface{}{"buildFuture": true})
|
||||
|
||||
if err != nil {
|
||||
return newSystemError("Error building sites", err)
|
||||
}
|
||||
@@ -126,7 +123,6 @@ List requires a subcommand, e.g. ` + "`hugo list drafts`.",
|
||||
Long: `List all of the posts in your content directory which has already expired.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
sites, err := cc.buildSites(map[string]interface{}{"buildExpired": true})
|
||||
|
||||
if err != nil {
|
||||
return newSystemError("Error building sites", err)
|
||||
}
|
||||
@@ -159,7 +155,6 @@ List requires a subcommand, e.g. ` + "`hugo list drafts`.",
|
||||
"buildDrafts": true,
|
||||
"buildFuture": true,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return newSystemError("Error building sites", err)
|
||||
}
|
||||
|
||||
+14
-3
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2020 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -20,6 +20,8 @@ import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
|
||||
"github.com/gohugoio/hugo/modules"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -89,7 +91,6 @@ Also note that if you configure a positive maxAge for the "modules" file cache,
|
||||
}
|
||||
|
||||
func (b *commandsBuilder) newModCmd() *modCmd {
|
||||
|
||||
c := &modCmd{}
|
||||
|
||||
const commonUsage = `
|
||||
@@ -114,6 +115,8 @@ This is not needed if you only operate on modules inside /themes or if you have
|
||||
RunE: nil,
|
||||
}
|
||||
|
||||
cmd.AddCommand(newModNPMCmd(c))
|
||||
|
||||
cmd.AddCommand(
|
||||
&cobra.Command{
|
||||
Use: "get",
|
||||
@@ -260,7 +263,6 @@ If a module is vendored, that is where Hugo will look for it's dependencies.
|
||||
c.baseBuilderCmd = b.newBuilderCmd(cmd)
|
||||
|
||||
return c
|
||||
|
||||
}
|
||||
|
||||
func (c *modCmd) withModsClient(failOnMissingConfig bool, f func(*modules.Client) error) error {
|
||||
@@ -272,6 +274,15 @@ func (c *modCmd) withModsClient(failOnMissingConfig bool, f func(*modules.Client
|
||||
return f(com.hugo().ModulesClient)
|
||||
}
|
||||
|
||||
func (c *modCmd) withHugo(f func(*hugolib.HugoSites) error) error {
|
||||
com, err := c.initConfig(true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return f(com.hugo())
|
||||
}
|
||||
|
||||
func (c *modCmd) initConfig(failOnNoConfig bool) (*commandeer, error) {
|
||||
com, err := initializeConfig(failOnNoConfig, false, &c.hugoBuilderCommon, c, nil)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright 2020 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
"github.com/gohugoio/hugo/modules/npm"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newModNPMCmd(c *modCmd) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "npm",
|
||||
Short: "Various npm helpers.",
|
||||
Long: `Various npm (Node package manager) helpers.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return c.withHugo(func(h *hugolib.HugoSites) error {
|
||||
return nil
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
cmd.AddCommand(&cobra.Command{
|
||||
Use: "pack",
|
||||
Short: "Experimental: Prepares and writes a composite package.json file for your project.",
|
||||
Long: `Prepares and writes a composite package.json file for your project.
|
||||
|
||||
On first run it creates a "package.hugo.json" in the project root if not already there. This file will be used as a template file
|
||||
with the base dependency set.
|
||||
|
||||
This set will be merged with all "package.hugo.json" files found in the dependency tree, picking the version closest to the project.
|
||||
|
||||
This command is marked as 'Experimental'. We think it's a great idea, so it's not likely to be
|
||||
removed from Hugo, but we need to test this out in "real life" to get a feel of it,
|
||||
so this may/will change in future versions of Hugo.
|
||||
`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return c.withHugo(func(h *hugolib.HugoSites) error {
|
||||
return npm.Pack(h.BaseFs.SourceFs, h.BaseFs.Assets.Dirs)
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
return cmd
|
||||
}
|
||||
+1
-2
@@ -72,7 +72,6 @@ func (n *newCmd) newContent(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
c, err := initializeConfig(true, false, &n.hugoBuilderCommon, n, cfgInit)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -119,7 +118,7 @@ func newContentPathSection(h *hugolib.HugoSites, path string) (string, string) {
|
||||
|
||||
if h != nil {
|
||||
for _, dir := range h.BaseFs.Content.Dirs {
|
||||
createpath = strings.TrimPrefix(createpath, dir.Meta().Filename())
|
||||
createpath = strings.TrimPrefix(createpath, dir.Meta().Filename)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/parser/metadecoders"
|
||||
|
||||
_errors "github.com/pkg/errors"
|
||||
@@ -29,7 +30,6 @@ import (
|
||||
"github.com/gohugoio/hugo/parser"
|
||||
"github.com/spf13/cobra"
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var _ cmder = (*newSiteCmd)(nil)
|
||||
@@ -58,7 +58,6 @@ Use ` + "`hugo new [contentPath]`" + ` to create new content.`,
|
||||
cc.baseBuilderCmd = b.newBuilderBasicCmd(cmd)
|
||||
|
||||
return cc
|
||||
|
||||
}
|
||||
|
||||
func (n *newSiteCmd) doNewSite(fs *hugofs.Fs, basepath string, force bool) error {
|
||||
@@ -124,7 +123,7 @@ func (n *newSiteCmd) newSite(cmd *cobra.Command, args []string) error {
|
||||
|
||||
forceNew, _ := cmd.Flags().GetBool("force")
|
||||
|
||||
return n.doNewSite(hugofs.NewDefault(viper.New()), createpath, forceNew)
|
||||
return n.doNewSite(hugofs.NewDefault(config.New()), createpath, forceNew)
|
||||
}
|
||||
|
||||
func createConfig(fs *hugofs.Fs, inpath string, kind string) (err error) {
|
||||
|
||||
@@ -38,7 +38,7 @@ func (b *commandsBuilder) newNewThemeCmd() *newThemeCmd {
|
||||
cmd := &cobra.Command{
|
||||
Use: "theme [name]",
|
||||
Short: "Create a new theme",
|
||||
Long: `Create a new theme (skeleton) called [name] in the current directory.
|
||||
Long: `Create a new theme (skeleton) called [name] in ./themes.
|
||||
New theme is a skeleton. Please add content to the touched files. Add your
|
||||
name to the copyright line in the license and adjust the theme.toml file
|
||||
as you see fit.`,
|
||||
@@ -53,7 +53,6 @@ as you see fit.`,
|
||||
// newTheme creates a new Hugo theme template
|
||||
func (n *newThemeCmd) newTheme(cmd *cobra.Command, args []string) error {
|
||||
c, err := initializeConfig(false, false, &n.hugoBuilderCommon, n, nil)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -145,7 +144,6 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
}
|
||||
|
||||
func (n *newThemeCmd) createThemeMD(fs *hugofs.Fs, inpath string) (err error) {
|
||||
|
||||
by := []byte(`# theme.toml template for a Hugo theme
|
||||
# See https://github.com/gohugoio/hugoThemes#themetoml for an example
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright 2019 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// +build nodeploy
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var _ cmder = (*deployCmd)(nil)
|
||||
|
||||
// deployCmd supports deploying sites to Cloud providers.
|
||||
type deployCmd struct {
|
||||
*baseBuilderCmd
|
||||
}
|
||||
|
||||
func (b *commandsBuilder) newDeployCmd() *deployCmd {
|
||||
cc := &deployCmd{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "deploy",
|
||||
Short: "Deploy your site to a Cloud provider.",
|
||||
Long: `Deploy your site to a Cloud provider.
|
||||
|
||||
See https://gohugo.io/hosting-and-deployment/hugo-deploy/ for detailed
|
||||
documentation.
|
||||
`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return errors.New("build without HUGO_BUILD_TAGS=nodeploy to use this command")
|
||||
},
|
||||
}
|
||||
|
||||
cc.baseBuilderCmd = b.newBuilderBasicCmd(cmd)
|
||||
|
||||
return cc
|
||||
}
|
||||
@@ -61,7 +61,6 @@ func (c *releaseCommandeer) getCommand() *cobra.Command {
|
||||
}
|
||||
|
||||
func (c *releaseCommandeer) flagsToConfig(cfg config.Provider) {
|
||||
|
||||
}
|
||||
|
||||
func (r *releaseCommandeer) release() error {
|
||||
|
||||
+90
-24
@@ -31,6 +31,8 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/common/paths"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/gohugoio/hugo/livereload"
|
||||
@@ -185,7 +187,7 @@ func (sc *serverCmd) server(cmd *cobra.Command, args []string) error {
|
||||
// port set explicitly by user -- he/she probably meant it!
|
||||
err = newSystemErrorF("Server startup failed: %s", err)
|
||||
}
|
||||
c.logger.FEEDBACK.Println("port", sc.serverPort, "already in use, attempting to use an available port")
|
||||
c.logger.Println("port", sc.serverPort, "already in use, attempting to use an available port")
|
||||
sp, err := helpers.FindAvailablePort()
|
||||
if err != nil {
|
||||
err = newSystemError("Unable to find alternative port to use:", err)
|
||||
@@ -228,19 +230,30 @@ func (sc *serverCmd) server(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
if err := memStats(); err != nil {
|
||||
jww.WARN.Println("memstats error:", err)
|
||||
}
|
||||
|
||||
// silence errors in cobra so we can handle them here
|
||||
cmd.SilenceErrors = true
|
||||
|
||||
c, err := initializeConfig(true, true, &sc.hugoBuilderCommon, sc, cfgInit)
|
||||
if err != nil {
|
||||
cmd.PrintErrln("Error:", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.serverBuild(); err != nil {
|
||||
err = func() error {
|
||||
defer c.timeTrack(time.Now(), "Built")
|
||||
err := c.serverBuild()
|
||||
if err != nil {
|
||||
cmd.PrintErrln("Error:", err.Error())
|
||||
}
|
||||
return err
|
||||
}()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -261,8 +274,7 @@ func (sc *serverCmd) server(cmd *cobra.Command, args []string) error {
|
||||
for _, group := range watchGroups {
|
||||
jww.FEEDBACK.Printf("Watching for changes in %s\n", group)
|
||||
}
|
||||
watcher, err := c.newWatcher(watchDirs...)
|
||||
|
||||
watcher, err := c.newWatcher(sc.poll, watchDirs...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -272,13 +284,12 @@ func (sc *serverCmd) server(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
return c.serve(sc)
|
||||
|
||||
}
|
||||
|
||||
func getRootWatchDirsStr(baseDir string, watchDirs []string) string {
|
||||
relWatchDirs := make([]string, len(watchDirs))
|
||||
for i, dir := range watchDirs {
|
||||
relWatchDirs[i], _ = helpers.GetRelativePath(dir, baseDir)
|
||||
relWatchDirs[i], _ = paths.GetRelativePath(dir, baseDir)
|
||||
}
|
||||
|
||||
return strings.Join(helpers.UniqueStringsSorted(helpers.ExtractRootPaths(relWatchDirs)), ",")
|
||||
@@ -292,6 +303,17 @@ type fileServer struct {
|
||||
s *serverCmd
|
||||
}
|
||||
|
||||
func (f *fileServer) rewriteRequest(r *http.Request, toPath string) *http.Request {
|
||||
r2 := new(http.Request)
|
||||
*r2 = *r
|
||||
r2.URL = new(url.URL)
|
||||
*r2.URL = *r.URL
|
||||
r2.URL.Path = toPath
|
||||
r2.Header.Set("X-Rewrite-Original-URI", r.URL.RequestURI())
|
||||
|
||||
return r2
|
||||
}
|
||||
|
||||
func (f *fileServer) createEndpoint(i int) (*http.ServeMux, string, string, error) {
|
||||
baseURL := f.baseURLs[i]
|
||||
root := f.roots[i]
|
||||
@@ -338,14 +360,16 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, string, string, erro
|
||||
w.WriteHeader(500)
|
||||
r, err := f.errorTemplate(err)
|
||||
if err != nil {
|
||||
f.c.logger.ERROR.Println(err)
|
||||
f.c.logger.Errorln(err)
|
||||
}
|
||||
|
||||
port = 1313
|
||||
if !f.c.paused {
|
||||
port = f.c.Cfg.GetInt("liveReloadPort")
|
||||
}
|
||||
fmt.Fprint(w, injectLiveReloadScript(r, port))
|
||||
lr := *u
|
||||
lr.Host = fmt.Sprintf("%s:%d", lr.Hostname(), port)
|
||||
fmt.Fprint(w, injectLiveReloadScript(r, lr))
|
||||
|
||||
return
|
||||
}
|
||||
@@ -356,36 +380,71 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, string, string, erro
|
||||
w.Header().Set("Pragma", "no-cache")
|
||||
}
|
||||
|
||||
for _, header := range f.c.serverConfig.Match(r.RequestURI) {
|
||||
// Ignore any query params for the operations below.
|
||||
requestURI := strings.TrimSuffix(r.RequestURI, "?"+r.URL.RawQuery)
|
||||
|
||||
for _, header := range f.c.serverConfig.MatchHeaders(requestURI) {
|
||||
w.Header().Set(header.Key, header.Value)
|
||||
}
|
||||
|
||||
if f.c.fastRenderMode && f.c.buildErr == nil {
|
||||
if redirect := f.c.serverConfig.MatchRedirect(requestURI); !redirect.IsZero() {
|
||||
doRedirect := true
|
||||
// This matches Netlify's behaviour and is needed for SPA behaviour.
|
||||
// See https://docs.netlify.com/routing/redirects/rewrites-proxies/
|
||||
if !redirect.Force {
|
||||
path := filepath.Clean(strings.TrimPrefix(requestURI, u.Path))
|
||||
fi, err := f.c.hugo().BaseFs.PublishFs.Stat(path)
|
||||
if err == nil {
|
||||
if fi.IsDir() {
|
||||
// There will be overlapping directories, so we
|
||||
// need to check for a file.
|
||||
_, err = f.c.hugo().BaseFs.PublishFs.Stat(filepath.Join(path, "index.html"))
|
||||
doRedirect = err != nil
|
||||
} else {
|
||||
doRedirect = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p := strings.TrimSuffix(r.RequestURI, "?"+r.URL.RawQuery)
|
||||
if strings.HasSuffix(p, "/") || strings.HasSuffix(p, "html") || strings.HasSuffix(p, "htm") {
|
||||
if !f.c.visitedURLs.Contains(p) {
|
||||
if doRedirect {
|
||||
if redirect.Status == 200 {
|
||||
if r2 := f.rewriteRequest(r, strings.TrimPrefix(redirect.To, u.Path)); r2 != nil {
|
||||
requestURI = redirect.To
|
||||
r = r2
|
||||
}
|
||||
} else {
|
||||
w.Header().Set("Content-Type", "")
|
||||
http.Redirect(w, r, redirect.To, redirect.Status)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if f.c.fastRenderMode && f.c.buildErr == nil {
|
||||
if strings.HasSuffix(requestURI, "/") || strings.HasSuffix(requestURI, "html") || strings.HasSuffix(requestURI, "htm") {
|
||||
if !f.c.visitedURLs.Contains(requestURI) {
|
||||
// If not already on stack, re-render that single page.
|
||||
if err := f.c.partialReRender(p); err != nil {
|
||||
f.c.handleBuildErr(err, fmt.Sprintf("Failed to render %q", p))
|
||||
if err := f.c.partialReRender(requestURI); err != nil {
|
||||
f.c.handleBuildErr(err, fmt.Sprintf("Failed to render %q", requestURI))
|
||||
if f.c.showErrorInBrowser {
|
||||
http.Redirect(w, r, p, http.StatusMovedPermanently)
|
||||
http.Redirect(w, r, requestURI, http.StatusMovedPermanently)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
f.c.visitedURLs.Add(p)
|
||||
f.c.visitedURLs.Add(requestURI)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
h.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
fileserver := decorate(http.FileServer(fs))
|
||||
mu := http.NewServeMux()
|
||||
|
||||
if u.Path == "" || u.Path == "/" {
|
||||
mu.Handle("/", fileserver)
|
||||
} else {
|
||||
@@ -402,8 +461,8 @@ var logErrorRe = regexp.MustCompile(`(?s)ERROR \d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{
|
||||
func removeErrorPrefixFromLog(content string) string {
|
||||
return logErrorRe.ReplaceAllLiteralString(content, "")
|
||||
}
|
||||
func (c *commandeer) serve(s *serverCmd) error {
|
||||
|
||||
func (c *commandeer) serve(s *serverCmd) error {
|
||||
isMultiHost := c.hugo().IsMultihost()
|
||||
|
||||
var (
|
||||
@@ -445,21 +504,26 @@ func (c *commandeer) serve(s *serverCmd) error {
|
||||
livereload.Initialize()
|
||||
}
|
||||
|
||||
var sigs = make(chan os.Signal, 1)
|
||||
sigs := make(chan os.Signal, 1)
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
for i := range baseURLs {
|
||||
mu, serverURL, endpoint, err := srv.createEndpoint(i)
|
||||
|
||||
if doLiveReload {
|
||||
mu.HandleFunc("/livereload.js", livereload.ServeJS)
|
||||
mu.HandleFunc("/livereload", livereload.Handler)
|
||||
u, err := url.Parse(helpers.SanitizeURL(baseURLs[i]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mu.HandleFunc(u.Path+"/livereload.js", livereload.ServeJS)
|
||||
mu.HandleFunc(u.Path+"/livereload", livereload.Handler)
|
||||
}
|
||||
jww.FEEDBACK.Printf("Web Server is available at %s (bind address %s)\n", serverURL, s.serverInterface)
|
||||
go func() {
|
||||
err = http.ListenAndServe(endpoint, mu)
|
||||
if err != nil {
|
||||
c.logger.ERROR.Printf("Error: %s\n", err.Error())
|
||||
c.logger.Errorf("Error: %s\n", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
@@ -476,6 +540,8 @@ func (c *commandeer) serve(s *serverCmd) error {
|
||||
<-sigs
|
||||
}
|
||||
|
||||
c.hugo().Close()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ package commands
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/url"
|
||||
|
||||
"github.com/gohugoio/hugo/transform"
|
||||
"github.com/gohugoio/hugo/transform/livereloadinject"
|
||||
@@ -82,9 +83,9 @@ var buildErrorTemplate = `<!doctype html>
|
||||
</html>
|
||||
`
|
||||
|
||||
func injectLiveReloadScript(src io.Reader, port int) string {
|
||||
func injectLiveReloadScript(src io.Reader, baseURL url.URL) string {
|
||||
var b bytes.Buffer
|
||||
chain := transform.Chain{livereloadinject.New(port)}
|
||||
chain := transform.Chain{livereloadinject.New(baseURL)}
|
||||
chain.Apply(&b, src)
|
||||
|
||||
return b.String()
|
||||
|
||||
@@ -22,10 +22,10 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func TestServer(t *testing.T) {
|
||||
@@ -73,7 +73,6 @@ func TestServer(t *testing.T) {
|
||||
|
||||
// Stop the server.
|
||||
stop <- true
|
||||
|
||||
}
|
||||
|
||||
func TestFixURL(t *testing.T) {
|
||||
@@ -102,7 +101,7 @@ func TestFixURL(t *testing.T) {
|
||||
t.Run(test.TestName, func(t *testing.T) {
|
||||
b := newCommandsBuilder()
|
||||
s := b.newServerCmd()
|
||||
v := viper.New()
|
||||
v := config.New()
|
||||
baseURL := test.CLIBaseURL
|
||||
v.Set("baseURL", test.CfgBaseURL)
|
||||
s.serverAppend = test.AppendPort
|
||||
@@ -127,7 +126,6 @@ ERROR 2018/10/07 13:11:12 Rebuild failed: logged 1 error(s)
|
||||
withoutError := removeErrorPrefixFromLog(content)
|
||||
|
||||
c.Assert(strings.Contains(withoutError, "ERROR"), qt.Equals, false)
|
||||
|
||||
}
|
||||
|
||||
func isWindowsCI() bool {
|
||||
|
||||
@@ -58,7 +58,7 @@ func (s *staticSyncer) syncsStaticEvents(staticEvents []fsnotify.Event) error {
|
||||
syncer.DestFs = c.Fs.Destination
|
||||
|
||||
// prevent spamming the log on changes
|
||||
logger := helpers.NewDistinctFeedbackLogger()
|
||||
logger := helpers.NewDistinctErrorLogger()
|
||||
|
||||
for _, ev := range staticEvents {
|
||||
// Due to our approach of layering both directories and the content's rendered output
|
||||
@@ -79,9 +79,9 @@ func (s *staticSyncer) syncsStaticEvents(staticEvents []fsnotify.Event) error {
|
||||
|
||||
fromPath := ev.Name
|
||||
|
||||
relPath := sourceFs.MakePathRelative(fromPath)
|
||||
relPath, found := sourceFs.MakePathRelative(fromPath)
|
||||
|
||||
if relPath == "" {
|
||||
if !found {
|
||||
// Not member of this virtual host.
|
||||
continue
|
||||
}
|
||||
@@ -107,10 +107,10 @@ func (s *staticSyncer) syncsStaticEvents(staticEvents []fsnotify.Event) error {
|
||||
logger.Println("Syncing", relPath, "to", publishDir)
|
||||
|
||||
if err := syncer.Sync(filepath.Join(publishDir, relPath), relPath); err != nil {
|
||||
c.logger.ERROR.Println(err)
|
||||
c.logger.Errorln(err)
|
||||
}
|
||||
} else {
|
||||
c.logger.ERROR.Println(err)
|
||||
c.logger.Errorln(err)
|
||||
}
|
||||
|
||||
continue
|
||||
@@ -119,7 +119,7 @@ func (s *staticSyncer) syncsStaticEvents(staticEvents []fsnotify.Event) error {
|
||||
// For all other event operations Hugo will sync static.
|
||||
logger.Println("Syncing", relPath, "to", publishDir)
|
||||
if err := syncer.Sync(filepath.Join(publishDir, relPath), relPath); err != nil {
|
||||
c.logger.ERROR.Println(err)
|
||||
c.logger.Errorln(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,5 +128,4 @@ func (s *staticSyncer) syncsStaticEvents(staticEvents []fsnotify.Event) error {
|
||||
|
||||
_, err := c.doWithPublishDirs(syncFn)
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
@@ -51,7 +51,6 @@ func Append(to interface{}, from ...interface{}) (interface{}, error) {
|
||||
} else if !fromt.AssignableTo(tot) {
|
||||
// Fall back to a []interface{} slice.
|
||||
return appendToInterfaceSliceFromValues(tov, fromv)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,28 +36,44 @@ func TestAppend(t *testing.T) {
|
||||
{nil, []interface{}{"a", "b"}, []string{"a", "b"}},
|
||||
{nil, []interface{}{nil}, []interface{}{nil}},
|
||||
{[]interface{}{}, []interface{}{[]string{"c", "d", "e"}}, []string{"c", "d", "e"}},
|
||||
{tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}},
|
||||
{
|
||||
tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}},
|
||||
[]interface{}{&tstSlicer{"c"}},
|
||||
tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}, &tstSlicer{"c"}}},
|
||||
{&tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}},
|
||||
tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}, &tstSlicer{"c"}},
|
||||
},
|
||||
{
|
||||
&tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}},
|
||||
[]interface{}{&tstSlicer{"c"}},
|
||||
tstSlicers{&tstSlicer{"a"},
|
||||
tstSlicers{
|
||||
&tstSlicer{"a"},
|
||||
&tstSlicer{"b"},
|
||||
&tstSlicer{"c"}}},
|
||||
{testSlicerInterfaces{&tstSlicerIn1{"a"}, &tstSlicerIn1{"b"}},
|
||||
&tstSlicer{"c"},
|
||||
},
|
||||
},
|
||||
{
|
||||
testSlicerInterfaces{&tstSlicerIn1{"a"}, &tstSlicerIn1{"b"}},
|
||||
[]interface{}{&tstSlicerIn1{"c"}},
|
||||
testSlicerInterfaces{&tstSlicerIn1{"a"}, &tstSlicerIn1{"b"}, &tstSlicerIn1{"c"}}},
|
||||
testSlicerInterfaces{&tstSlicerIn1{"a"}, &tstSlicerIn1{"b"}, &tstSlicerIn1{"c"}},
|
||||
},
|
||||
//https://github.com/gohugoio/hugo/issues/5361
|
||||
{[]string{"a", "b"}, []interface{}{tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}}},
|
||||
[]interface{}{"a", "b", &tstSlicer{"a"}, &tstSlicer{"b"}}},
|
||||
{[]string{"a", "b"}, []interface{}{&tstSlicer{"a"}},
|
||||
[]interface{}{"a", "b", &tstSlicer{"a"}}},
|
||||
{
|
||||
[]string{"a", "b"},
|
||||
[]interface{}{tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}}},
|
||||
[]interface{}{"a", "b", &tstSlicer{"a"}, &tstSlicer{"b"}},
|
||||
},
|
||||
{
|
||||
[]string{"a", "b"},
|
||||
[]interface{}{&tstSlicer{"a"}},
|
||||
[]interface{}{"a", "b", &tstSlicer{"a"}},
|
||||
},
|
||||
// Errors
|
||||
{"", []interface{}{[]string{"a", "b"}}, false},
|
||||
// No string concatenation.
|
||||
{"ab",
|
||||
{
|
||||
"ab",
|
||||
[]interface{}{"c"},
|
||||
false},
|
||||
false,
|
||||
},
|
||||
} {
|
||||
|
||||
result, err := Append(test.start, test.addend...)
|
||||
@@ -71,5 +87,4 @@ func TestAppend(t *testing.T) {
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(result, qt.DeepEquals, test.expected)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright 2020 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package collections
|
||||
|
||||
type Order interface {
|
||||
// Ordinal is a zero-based ordinal that represents the order of an object
|
||||
// in a collection.
|
||||
Ordinal() int
|
||||
}
|
||||
@@ -20,11 +20,13 @@ import (
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
var _ Slicer = (*tstSlicer)(nil)
|
||||
var _ Slicer = (*tstSlicerIn1)(nil)
|
||||
var _ Slicer = (*tstSlicerIn2)(nil)
|
||||
var _ testSlicerInterface = (*tstSlicerIn1)(nil)
|
||||
var _ testSlicerInterface = (*tstSlicerIn1)(nil)
|
||||
var (
|
||||
_ Slicer = (*tstSlicer)(nil)
|
||||
_ Slicer = (*tstSlicerIn1)(nil)
|
||||
_ Slicer = (*tstSlicerIn2)(nil)
|
||||
_ testSlicerInterface = (*tstSlicerIn1)(nil)
|
||||
_ testSlicerInterface = (*tstSlicerIn1)(nil)
|
||||
)
|
||||
|
||||
type testSlicerInterface interface {
|
||||
Name() string
|
||||
@@ -54,7 +56,6 @@ func (p *tstSlicerIn1) Slice(in interface{}) (interface{}, error) {
|
||||
default:
|
||||
return nil, errors.New("invalid type")
|
||||
}
|
||||
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -120,5 +121,4 @@ func TestSlice(t *testing.T) {
|
||||
|
||||
c.Assert(test.expected, qt.DeepEquals, result, errMsg)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright 2020 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package constants
|
||||
|
||||
// Error IDs.
|
||||
// Do not change these values.
|
||||
const (
|
||||
ErrIDAmbigousDisableKindTaxonomy = "error-disable-taxonomy"
|
||||
ErrIDAmbigousOutputKindTaxonomy = "error-output-taxonomy"
|
||||
|
||||
// IDs for remote errors in tpl/data.
|
||||
ErrRemoteGetJSON = "error-remote-getjson"
|
||||
ErrRemoteGetCSV = "error-remote-getcsv"
|
||||
)
|
||||
@@ -125,5 +125,4 @@ E`, offsetMatcher)
|
||||
c.Assert(location.Lines, qt.DeepEquals, []string{"A", "B", "C", "D"})
|
||||
c.Assert(location.Position().LineNumber, qt.Equals, 2)
|
||||
c.Assert(location.LinesPos, qt.Equals, 1)
|
||||
|
||||
}
|
||||
|
||||
@@ -21,9 +21,7 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
_ causer = (*fileError)(nil)
|
||||
)
|
||||
var _ causer = (*fileError)(nil)
|
||||
|
||||
// FileError represents an error when handling a file: Parsing a config file,
|
||||
// execute a template etc.
|
||||
|
||||
@@ -52,5 +52,4 @@ func TestToLineNumberError(t *testing.T) {
|
||||
c.Assert(pos.ColumnNumber, qt.Equals, test.columnNumber, errMsg)
|
||||
c.Assert(errors.Cause(got), qt.Not(qt.IsNil))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright 2020 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package hexec
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"os/exec"
|
||||
|
||||
"github.com/cli/safeexec"
|
||||
)
|
||||
|
||||
// SafeCommand is a wrapper around os/exec Command which uses a LookPath
|
||||
// implementation that does not search in current directory before looking in PATH.
|
||||
// See https://github.com/cli/safeexec and the linked issues.
|
||||
func SafeCommand(name string, arg ...string) (*exec.Cmd, error) {
|
||||
bin, err := safeexec.LookPath(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return exec.Command(bin, arg...), nil
|
||||
}
|
||||
|
||||
// SafeCommandContext wraps CommandContext
|
||||
// See SafeCommand for more context.
|
||||
func SafeCommandContext(ctx context.Context, name string, arg ...string) (*exec.Cmd, error) {
|
||||
bin, err := safeexec.LookPath(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return exec.CommandContext(ctx, bin, arg...), nil
|
||||
}
|
||||
@@ -22,6 +22,42 @@ import (
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
)
|
||||
|
||||
// TODO(bep) replace the private versions in /tpl with these.
|
||||
// IsNumber returns whether the given kind is a number.
|
||||
func IsNumber(kind reflect.Kind) bool {
|
||||
return IsInt(kind) || IsUint(kind) || IsFloat(kind)
|
||||
}
|
||||
|
||||
// IsInt returns whether the given kind is an int.
|
||||
func IsInt(kind reflect.Kind) bool {
|
||||
switch kind {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// IsUint returns whether the given kind is an uint.
|
||||
func IsUint(kind reflect.Kind) bool {
|
||||
switch kind {
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// IsFloat returns whether the given kind is a float.
|
||||
func IsFloat(kind reflect.Kind) bool {
|
||||
switch kind {
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// IsTruthful returns whether in represents a truthful value.
|
||||
// See IsTruthfulValue
|
||||
func IsTruthful(in interface{}) bool {
|
||||
@@ -31,7 +67,6 @@ func IsTruthful(in interface{}) bool {
|
||||
default:
|
||||
return IsTruthfulValue(reflect.ValueOf(in))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var zeroType = reflect.TypeOf((*types.Zeroer)(nil)).Elem()
|
||||
|
||||
+13
-12
@@ -26,28 +26,29 @@ import (
|
||||
|
||||
// CopyFile copies a file.
|
||||
func CopyFile(fs afero.Fs, from, to string) error {
|
||||
sf, err := os.Open(from)
|
||||
sf, err := fs.Open(from)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer sf.Close()
|
||||
df, err := os.Create(to)
|
||||
df, err := fs.Create(to)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer df.Close()
|
||||
_, err = io.Copy(df, sf)
|
||||
if err == nil {
|
||||
si, err := os.Stat(from)
|
||||
if err != nil {
|
||||
err = os.Chmod(to, si.Mode())
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
si, err := fs.Stat(from)
|
||||
if err != nil {
|
||||
err = fs.Chmod(to, si.Mode())
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ type ReadSeekCloser interface {
|
||||
}
|
||||
|
||||
// ReadSeekerNoOpCloser implements ReadSeekCloser by doing nothing in Close.
|
||||
// TODO(bep) rename this and simila to ReadSeekerNopCloser, naming used in stdlib, which kind of makes sense.
|
||||
// TODO(bep) rename this and similar to ReadSeekerNopCloser, naming used in stdlib, which kind of makes sense.
|
||||
type ReadSeekerNoOpCloser struct {
|
||||
ReadSeeker
|
||||
}
|
||||
|
||||
+76
-3
@@ -17,8 +17,17 @@ import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime/debug"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/hugofs/files"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -27,12 +36,15 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
// commitHash contains the current Git revision. Use make to build to make
|
||||
// sure this gets set.
|
||||
// commitHash contains the current Git revision.
|
||||
// Use mage to build to make sure this gets set.
|
||||
commitHash string
|
||||
|
||||
// buildDate contains the date of the current build.
|
||||
buildDate string
|
||||
|
||||
// vendorInfo contains vendor notes about the current build.
|
||||
vendorInfo string
|
||||
)
|
||||
|
||||
// Info contains information about the current Hugo environment
|
||||
@@ -61,6 +73,10 @@ func (i Info) IsProduction() bool {
|
||||
return i.Environment == EnvironmentProduction
|
||||
}
|
||||
|
||||
func (i Info) IsExtended() bool {
|
||||
return IsExtended
|
||||
}
|
||||
|
||||
// NewInfo creates a new Hugo Info object.
|
||||
func NewInfo(environment string) Info {
|
||||
if environment == "" {
|
||||
@@ -73,8 +89,65 @@ func NewInfo(environment string) Info {
|
||||
}
|
||||
}
|
||||
|
||||
func GetExecEnviron(cfg config.Provider) []string {
|
||||
func GetExecEnviron(workDir string, cfg config.Provider, fs afero.Fs) []string {
|
||||
env := os.Environ()
|
||||
nodepath := filepath.Join(workDir, "node_modules")
|
||||
if np := os.Getenv("NODE_PATH"); np != "" {
|
||||
nodepath = workDir + string(os.PathListSeparator) + np
|
||||
}
|
||||
config.SetEnvVars(&env, "NODE_PATH", nodepath)
|
||||
config.SetEnvVars(&env, "PWD", workDir)
|
||||
config.SetEnvVars(&env, "HUGO_ENVIRONMENT", cfg.GetString("environment"))
|
||||
fis, err := afero.ReadDir(fs, files.FolderJSConfig)
|
||||
if err == nil {
|
||||
for _, fi := range fis {
|
||||
key := fmt.Sprintf("HUGO_FILE_%s", strings.ReplaceAll(strings.ToUpper(fi.Name()), ".", "_"))
|
||||
value := fi.(hugofs.FileMetaInfo).Meta().Filename
|
||||
config.SetEnvVars(&env, key, value)
|
||||
}
|
||||
}
|
||||
|
||||
return env
|
||||
}
|
||||
|
||||
// GetDependencyList returns a sorted dependency list on the format package="version".
|
||||
// It includes both Go dependencies and (a manually maintained) list of C(++) dependencies.
|
||||
func GetDependencyList() []string {
|
||||
var deps []string
|
||||
|
||||
formatDep := func(path, version string) string {
|
||||
return fmt.Sprintf("%s=%q", path, version)
|
||||
}
|
||||
|
||||
if IsExtended {
|
||||
deps = append(
|
||||
deps,
|
||||
// TODO(bep) consider adding a DepsNonGo() method to these upstream projects.
|
||||
formatDep("github.com/sass/libsass", "3.6.5"),
|
||||
formatDep("github.com/webmproject/libwebp", "v1.2.0"),
|
||||
)
|
||||
}
|
||||
|
||||
bi, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
return deps
|
||||
}
|
||||
|
||||
for _, dep := range bi.Deps {
|
||||
deps = append(deps, formatDep(dep.Path, dep.Version))
|
||||
}
|
||||
|
||||
sort.Strings(deps)
|
||||
|
||||
return deps
|
||||
}
|
||||
|
||||
// IsRunningAsTest reports whether we are running as a test.
|
||||
func IsRunningAsTest() bool {
|
||||
for _, arg := range os.Args {
|
||||
if strings.HasPrefix(arg, "-test") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -32,8 +32,8 @@ func TestHugoInfo(t *testing.T) {
|
||||
c.Assert(hugoInfo.Environment, qt.Equals, "production")
|
||||
c.Assert(string(hugoInfo.Generator()), qt.Contains, fmt.Sprintf("Hugo %s", hugoInfo.Version()))
|
||||
c.Assert(hugoInfo.IsProduction(), qt.Equals, true)
|
||||
c.Assert(hugoInfo.IsExtended(), qt.Equals, IsExtended)
|
||||
|
||||
devHugoInfo := NewInfo("development")
|
||||
c.Assert(devHugoInfo.IsProduction(), qt.Equals, false)
|
||||
|
||||
}
|
||||
|
||||
+10
-4
@@ -16,7 +16,6 @@ package hugo
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
@@ -128,14 +127,15 @@ func (v Version) NextPatchLevel(level int) Version {
|
||||
// BuildVersionString creates a version string. This is what you see when
|
||||
// running "hugo version".
|
||||
func BuildVersionString() string {
|
||||
program := "Hugo Static Site Generator"
|
||||
// program := "Hugo Static Site Generator"
|
||||
program := "hugo"
|
||||
|
||||
version := "v" + CurrentVersion.String()
|
||||
if commitHash != "" {
|
||||
version += "-" + strings.ToUpper(commitHash)
|
||||
}
|
||||
if IsExtended {
|
||||
version += "/extended"
|
||||
version += "+extended"
|
||||
}
|
||||
|
||||
osArch := runtime.GOOS + "/" + runtime.GOARCH
|
||||
@@ -145,8 +145,14 @@ func BuildVersionString() string {
|
||||
date = "unknown"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s %s %s BuildDate: %s", program, version, osArch, date)
|
||||
versionString := fmt.Sprintf("%s %s %s BuildDate=%s",
|
||||
program, version, osArch, date)
|
||||
|
||||
if vendorInfo != "" {
|
||||
versionString += " VendorInfo=" + vendorInfo
|
||||
}
|
||||
|
||||
return versionString
|
||||
}
|
||||
|
||||
func version(version float32, patchVersion int, suffix string) string {
|
||||
|
||||
@@ -16,7 +16,7 @@ package hugo
|
||||
// CurrentVersion represents the current build version.
|
||||
// This should be the only one.
|
||||
var CurrentVersion = Version{
|
||||
Number: 0.71,
|
||||
PatchLevel: 1,
|
||||
Number: 0.86,
|
||||
PatchLevel: 0,
|
||||
Suffix: "",
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// Copyright 2020 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package loggers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// IgnorableLogger is a logger that ignores certain log statements.
|
||||
type IgnorableLogger interface {
|
||||
Logger
|
||||
Errorsf(statementID, format string, v ...interface{})
|
||||
Apply(logger Logger) IgnorableLogger
|
||||
}
|
||||
|
||||
type ignorableLogger struct {
|
||||
Logger
|
||||
statements map[string]bool
|
||||
}
|
||||
|
||||
// NewIgnorableLogger wraps the given logger and ignores the log statement IDs given.
|
||||
func NewIgnorableLogger(logger Logger, statements ...string) IgnorableLogger {
|
||||
statementsSet := make(map[string]bool)
|
||||
for _, s := range statements {
|
||||
statementsSet[strings.ToLower(s)] = true
|
||||
}
|
||||
return ignorableLogger{
|
||||
Logger: logger,
|
||||
statements: statementsSet,
|
||||
}
|
||||
}
|
||||
|
||||
// Errorsf logs statementID as an ERROR if not configured as ignoreable.
|
||||
func (l ignorableLogger) Errorsf(statementID, format string, v ...interface{}) {
|
||||
if l.statements[statementID] {
|
||||
// Ignore.
|
||||
return
|
||||
}
|
||||
ignoreMsg := fmt.Sprintf(`
|
||||
If you feel that this should not be logged as an ERROR, you can ignore it by adding this to your site config:
|
||||
ignoreErrors = [%q]`, statementID)
|
||||
|
||||
format += ignoreMsg
|
||||
|
||||
l.Errorf(format, v...)
|
||||
}
|
||||
|
||||
func (l ignorableLogger) Apply(logger Logger) IgnorableLogger {
|
||||
return ignorableLogger{
|
||||
Logger: logger,
|
||||
statements: l.statements,
|
||||
}
|
||||
}
|
||||
+156
-38
@@ -1,4 +1,4 @@
|
||||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2020 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -29,49 +29,157 @@ import (
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
)
|
||||
|
||||
var (
|
||||
// Counts ERROR logs to the global jww logger.
|
||||
GlobalErrorCounter *jww.Counter
|
||||
)
|
||||
// Counts ERROR logs to the global jww logger.
|
||||
var GlobalErrorCounter *jww.Counter
|
||||
|
||||
func init() {
|
||||
GlobalErrorCounter = &jww.Counter{}
|
||||
jww.SetLogListeners(jww.LogCounter(GlobalErrorCounter, jww.LevelError))
|
||||
}
|
||||
|
||||
// Logger wraps a *loggers.Logger and some other related logging state.
|
||||
type Logger struct {
|
||||
func LoggerToWriterWithPrefix(logger *log.Logger, prefix string) io.Writer {
|
||||
return prefixWriter{
|
||||
logger: logger,
|
||||
prefix: prefix,
|
||||
}
|
||||
}
|
||||
|
||||
type prefixWriter struct {
|
||||
logger *log.Logger
|
||||
prefix string
|
||||
}
|
||||
|
||||
func (w prefixWriter) Write(p []byte) (n int, err error) {
|
||||
w.logger.Printf("%s: %s", w.prefix, p)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
type Logger interface {
|
||||
Printf(format string, v ...interface{})
|
||||
Println(v ...interface{})
|
||||
PrintTimerIfDelayed(start time.Time, name string)
|
||||
Debug() *log.Logger
|
||||
Debugf(format string, v ...interface{})
|
||||
Debugln(v ...interface{})
|
||||
Info() *log.Logger
|
||||
Infof(format string, v ...interface{})
|
||||
Infoln(v ...interface{})
|
||||
Warn() *log.Logger
|
||||
Warnf(format string, v ...interface{})
|
||||
Warnln(v ...interface{})
|
||||
Error() *log.Logger
|
||||
Errorf(format string, v ...interface{})
|
||||
Errorln(v ...interface{})
|
||||
Errors() string
|
||||
|
||||
Out() io.Writer
|
||||
|
||||
Reset()
|
||||
|
||||
// Used in tests.
|
||||
LogCounters() *LogCounters
|
||||
}
|
||||
|
||||
type LogCounters struct {
|
||||
ErrorCounter *jww.Counter
|
||||
WarnCounter *jww.Counter
|
||||
}
|
||||
|
||||
type logger struct {
|
||||
*jww.Notepad
|
||||
|
||||
// The writer that represents stdout.
|
||||
// Will be ioutil.Discard when in quiet mode.
|
||||
Out io.Writer
|
||||
out io.Writer
|
||||
|
||||
ErrorCounter *jww.Counter
|
||||
WarnCounter *jww.Counter
|
||||
logCounters *LogCounters
|
||||
|
||||
// This is only set in server mode.
|
||||
errors *bytes.Buffer
|
||||
}
|
||||
|
||||
func (l *logger) Printf(format string, v ...interface{}) {
|
||||
l.FEEDBACK.Printf(format, v...)
|
||||
}
|
||||
|
||||
func (l *logger) Println(v ...interface{}) {
|
||||
l.FEEDBACK.Println(v...)
|
||||
}
|
||||
|
||||
func (l *logger) Debug() *log.Logger {
|
||||
return l.DEBUG
|
||||
}
|
||||
|
||||
func (l *logger) Debugf(format string, v ...interface{}) {
|
||||
l.DEBUG.Printf(format, v...)
|
||||
}
|
||||
|
||||
func (l *logger) Debugln(v ...interface{}) {
|
||||
l.DEBUG.Println(v...)
|
||||
}
|
||||
|
||||
func (l *logger) Infof(format string, v ...interface{}) {
|
||||
l.INFO.Printf(format, v...)
|
||||
}
|
||||
|
||||
func (l *logger) Infoln(v ...interface{}) {
|
||||
l.INFO.Println(v...)
|
||||
}
|
||||
|
||||
func (l *logger) Info() *log.Logger {
|
||||
return l.INFO
|
||||
}
|
||||
|
||||
func (l *logger) Warnf(format string, v ...interface{}) {
|
||||
l.WARN.Printf(format, v...)
|
||||
}
|
||||
|
||||
func (l *logger) Warnln(v ...interface{}) {
|
||||
l.WARN.Println(v...)
|
||||
}
|
||||
|
||||
func (l *logger) Warn() *log.Logger {
|
||||
return l.WARN
|
||||
}
|
||||
|
||||
func (l *logger) Errorf(format string, v ...interface{}) {
|
||||
l.ERROR.Printf(format, v...)
|
||||
}
|
||||
|
||||
func (l *logger) Errorln(v ...interface{}) {
|
||||
l.ERROR.Println(v...)
|
||||
}
|
||||
|
||||
func (l *logger) Error() *log.Logger {
|
||||
return l.ERROR
|
||||
}
|
||||
|
||||
func (l *logger) LogCounters() *LogCounters {
|
||||
return l.logCounters
|
||||
}
|
||||
|
||||
func (l *logger) Out() io.Writer {
|
||||
return l.out
|
||||
}
|
||||
|
||||
// PrintTimerIfDelayed prints a time statement to the FEEDBACK logger
|
||||
// if considerable time is spent.
|
||||
func (l *Logger) PrintTimerIfDelayed(start time.Time, name string) {
|
||||
func (l *logger) PrintTimerIfDelayed(start time.Time, name string) {
|
||||
elapsed := time.Since(start)
|
||||
milli := int(1000 * elapsed.Seconds())
|
||||
if milli < 500 {
|
||||
return
|
||||
}
|
||||
l.FEEDBACK.Printf("%s in %v ms", name, milli)
|
||||
l.Printf("%s in %v ms", name, milli)
|
||||
}
|
||||
|
||||
func (l *Logger) PrintTimer(start time.Time, name string) {
|
||||
func (l *logger) PrintTimer(start time.Time, name string) {
|
||||
elapsed := time.Since(start)
|
||||
milli := int(1000 * elapsed.Seconds())
|
||||
l.FEEDBACK.Printf("%s in %v ms", name, milli)
|
||||
l.Printf("%s in %v ms", name, milli)
|
||||
}
|
||||
|
||||
func (l *Logger) Errors() string {
|
||||
func (l *logger) Errors() string {
|
||||
if l.errors == nil {
|
||||
return ""
|
||||
}
|
||||
@@ -79,31 +187,46 @@ func (l *Logger) Errors() string {
|
||||
}
|
||||
|
||||
// Reset resets the logger's internal state.
|
||||
func (l *Logger) Reset() {
|
||||
l.ErrorCounter.Reset()
|
||||
func (l *logger) Reset() {
|
||||
l.logCounters.ErrorCounter.Reset()
|
||||
if l.errors != nil {
|
||||
l.errors.Reset()
|
||||
}
|
||||
}
|
||||
|
||||
// NewLogger creates a new Logger for the given thresholds
|
||||
func NewLogger(stdoutThreshold, logThreshold jww.Threshold, outHandle, logHandle io.Writer, saveErrors bool) *Logger {
|
||||
func NewLogger(stdoutThreshold, logThreshold jww.Threshold, outHandle, logHandle io.Writer, saveErrors bool) Logger {
|
||||
return newLogger(stdoutThreshold, logThreshold, outHandle, logHandle, saveErrors)
|
||||
}
|
||||
|
||||
// NewDebugLogger is a convenience function to create a debug logger.
|
||||
func NewDebugLogger() *Logger {
|
||||
return newBasicLogger(jww.LevelDebug)
|
||||
func NewDebugLogger() Logger {
|
||||
return NewBasicLogger(jww.LevelDebug)
|
||||
}
|
||||
|
||||
// NewWarningLogger is a convenience function to create a warning logger.
|
||||
func NewWarningLogger() *Logger {
|
||||
return newBasicLogger(jww.LevelWarn)
|
||||
func NewWarningLogger() Logger {
|
||||
return NewBasicLogger(jww.LevelWarn)
|
||||
}
|
||||
|
||||
// NewInfoLogger is a convenience function to create a info logger.
|
||||
func NewInfoLogger() Logger {
|
||||
return NewBasicLogger(jww.LevelInfo)
|
||||
}
|
||||
|
||||
// NewErrorLogger is a convenience function to create an error logger.
|
||||
func NewErrorLogger() *Logger {
|
||||
return newBasicLogger(jww.LevelError)
|
||||
func NewErrorLogger() Logger {
|
||||
return NewBasicLogger(jww.LevelError)
|
||||
}
|
||||
|
||||
// NewBasicLogger creates a new basic logger writing to Stdout.
|
||||
func NewBasicLogger(t jww.Threshold) Logger {
|
||||
return newLogger(t, jww.LevelError, os.Stdout, ioutil.Discard, false)
|
||||
}
|
||||
|
||||
// NewBasicLoggerForWriter creates a new basic logger writing to w.
|
||||
func NewBasicLoggerForWriter(t jww.Threshold, w io.Writer) Logger {
|
||||
return newLogger(t, jww.LevelError, w, ioutil.Discard, false)
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -138,7 +261,6 @@ func (a labelColorizer) Write(p []byte) (n int, err error) {
|
||||
// bytes, so we lie a little.
|
||||
_, err = a.w.Write([]byte(replaced))
|
||||
return len(p), err
|
||||
|
||||
}
|
||||
|
||||
// InitGlobalLogger initializes the global logger, used in some rare cases.
|
||||
@@ -149,7 +271,6 @@ func InitGlobalLogger(stdoutThreshold, logThreshold jww.Threshold, outHandle, lo
|
||||
jww.SetLogOutput(logHandle)
|
||||
jww.SetLogThreshold(logThreshold)
|
||||
jww.SetStdoutThreshold(stdoutThreshold)
|
||||
|
||||
}
|
||||
|
||||
func getLogWriters(outHandle, logHandle io.Writer) (io.Writer, io.Writer) {
|
||||
@@ -164,7 +285,6 @@ func getLogWriters(outHandle, logHandle io.Writer) (io.Writer, io.Writer) {
|
||||
}
|
||||
|
||||
return outHandle, logHandle
|
||||
|
||||
}
|
||||
|
||||
type fatalLogWriter int
|
||||
@@ -187,7 +307,7 @@ var fatalLogListener = func(t jww.Threshold) io.Writer {
|
||||
return new(fatalLogWriter)
|
||||
}
|
||||
|
||||
func newLogger(stdoutThreshold, logThreshold jww.Threshold, outHandle, logHandle io.Writer, saveErrors bool) *Logger {
|
||||
func newLogger(stdoutThreshold, logThreshold jww.Threshold, outHandle, logHandle io.Writer, saveErrors bool) *logger {
|
||||
errorCounter := &jww.Counter{}
|
||||
warnCounter := &jww.Counter{}
|
||||
outHandle, logHandle = getLogWriters(outHandle, logHandle)
|
||||
@@ -207,15 +327,13 @@ func newLogger(stdoutThreshold, logThreshold jww.Threshold, outHandle, logHandle
|
||||
listeners = append(listeners, errorCapture)
|
||||
}
|
||||
|
||||
return &Logger{
|
||||
Notepad: jww.NewNotepad(stdoutThreshold, logThreshold, outHandle, logHandle, "", log.Ldate|log.Ltime, listeners...),
|
||||
Out: outHandle,
|
||||
ErrorCounter: errorCounter,
|
||||
WarnCounter: warnCounter,
|
||||
errors: errorBuff,
|
||||
return &logger{
|
||||
Notepad: jww.NewNotepad(stdoutThreshold, logThreshold, outHandle, logHandle, "", log.Ldate|log.Ltime, listeners...),
|
||||
out: outHandle,
|
||||
logCounters: &LogCounters{
|
||||
ErrorCounter: errorCounter,
|
||||
WarnCounter: warnCounter,
|
||||
},
|
||||
errors: errorBuff,
|
||||
}
|
||||
}
|
||||
|
||||
func newBasicLogger(t jww.Threshold) *Logger {
|
||||
return newLogger(t, jww.LevelError, os.Stdout, ioutil.Discard, false)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
package loggers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
@@ -23,10 +26,23 @@ func TestLogger(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
l := NewWarningLogger()
|
||||
|
||||
l.ERROR.Println("One error")
|
||||
l.ERROR.Println("Two error")
|
||||
l.WARN.Println("A warning")
|
||||
|
||||
c.Assert(l.ErrorCounter.Count(), qt.Equals, uint64(2))
|
||||
l.Errorln("One error")
|
||||
l.Errorln("Two error")
|
||||
l.Warnln("A warning")
|
||||
|
||||
c.Assert(l.LogCounters().ErrorCounter.Count(), qt.Equals, uint64(2))
|
||||
}
|
||||
|
||||
func TestLoggerToWriterWithPrefix(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
var b bytes.Buffer
|
||||
|
||||
logger := log.New(&b, "", 0)
|
||||
|
||||
w := LoggerToWriterWithPrefix(logger, "myprefix")
|
||||
|
||||
fmt.Fprint(w, "Hello Hugo!")
|
||||
|
||||
c.Assert(b.String(), qt.Equals, "myprefix: Hello Hugo!\n")
|
||||
}
|
||||
|
||||
+72
-34
@@ -14,56 +14,95 @@
|
||||
package maps
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gobwas/glob"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
// ToLower makes all the keys in the given map lower cased and will do so
|
||||
// recursively.
|
||||
// Notes:
|
||||
// * This will modify the map given.
|
||||
// * Any nested map[interface{}]interface{} will be converted to Params.
|
||||
func ToLower(m Params) {
|
||||
for k, v := range m {
|
||||
var retyped bool
|
||||
switch v.(type) {
|
||||
case map[interface{}]interface{}:
|
||||
var p Params = cast.ToStringMap(v)
|
||||
v = p
|
||||
ToLower(p)
|
||||
retyped = true
|
||||
case map[string]interface{}:
|
||||
var p Params = v.(map[string]interface{})
|
||||
v = p
|
||||
ToLower(p)
|
||||
retyped = true
|
||||
}
|
||||
|
||||
lKey := strings.ToLower(k)
|
||||
if retyped || k != lKey {
|
||||
delete(m, k)
|
||||
m[lKey] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ToStringMapE converts in to map[string]interface{}.
|
||||
func ToStringMapE(in interface{}) (map[string]interface{}, error) {
|
||||
switch in.(type) {
|
||||
switch vv := in.(type) {
|
||||
case Params:
|
||||
return in.(Params), nil
|
||||
return vv, nil
|
||||
case map[string]string:
|
||||
var m = map[string]interface{}{}
|
||||
for k, v := range vv {
|
||||
m[k] = v
|
||||
}
|
||||
return m, nil
|
||||
|
||||
default:
|
||||
return cast.ToStringMapE(in)
|
||||
}
|
||||
}
|
||||
|
||||
// ToParamsAndPrepare converts in to Params and prepares it for use.
|
||||
// See PrepareParams.
|
||||
func ToParamsAndPrepare(in interface{}) (Params, bool) {
|
||||
m, err := ToStringMapE(in)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
PrepareParams(m)
|
||||
return m, true
|
||||
}
|
||||
|
||||
// MustToParamsAndPrepare calls ToParamsAndPrepare and panics if it fails.
|
||||
func MustToParamsAndPrepare(in interface{}) Params {
|
||||
if p, ok := ToParamsAndPrepare(in); ok {
|
||||
return p
|
||||
} else {
|
||||
panic(fmt.Sprintf("cannot convert %T to maps.Params", in))
|
||||
}
|
||||
}
|
||||
|
||||
// ToStringMap converts in to map[string]interface{}.
|
||||
func ToStringMap(in interface{}) map[string]interface{} {
|
||||
m, _ := ToStringMapE(in)
|
||||
return m
|
||||
}
|
||||
|
||||
// ToStringMapStringE converts in to map[string]string.
|
||||
func ToStringMapStringE(in interface{}) (map[string]string, error) {
|
||||
m, err := ToStringMapE(in)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cast.ToStringMapStringE(m)
|
||||
}
|
||||
|
||||
// ToStringMapString converts in to map[string]string.
|
||||
func ToStringMapString(in interface{}) map[string]string {
|
||||
m, _ := ToStringMapStringE(in)
|
||||
return m
|
||||
}
|
||||
|
||||
// ToStringMapBool converts in to bool.
|
||||
func ToStringMapBool(in interface{}) map[string]bool {
|
||||
m, _ := ToStringMapE(in)
|
||||
return cast.ToStringMapBool(m)
|
||||
}
|
||||
|
||||
// ToSliceStringMap converts in to []map[string]interface{}.
|
||||
func ToSliceStringMap(in interface{}) ([]map[string]interface{}, error) {
|
||||
switch v := in.(type) {
|
||||
case []map[string]interface{}:
|
||||
return v, nil
|
||||
case []interface{}:
|
||||
var s []map[string]interface{}
|
||||
for _, entry := range v {
|
||||
if vv, ok := entry.(map[string]interface{}); ok {
|
||||
s = append(s, vv)
|
||||
}
|
||||
}
|
||||
return s, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unable to cast %#v of type %T to []map[string]interface{}", in, in)
|
||||
}
|
||||
}
|
||||
|
||||
type keyRename struct {
|
||||
pattern glob.Glob
|
||||
newKey string
|
||||
@@ -109,9 +148,8 @@ func (KeyRenamer) keyPath(k1, k2 string) string {
|
||||
k1, k2 = strings.ToLower(k1), strings.ToLower(k2)
|
||||
if k1 == "" {
|
||||
return k2
|
||||
} else {
|
||||
return k1 + "/" + k2
|
||||
}
|
||||
return k1 + "/" + k2
|
||||
}
|
||||
|
||||
func (r KeyRenamer) renamePath(parentKeyPath string, m map[string]interface{}) {
|
||||
|
||||
@@ -21,10 +21,10 @@ import (
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestToLower(t *testing.T) {
|
||||
func TestPrepareParams(t *testing.T) {
|
||||
tests := []struct {
|
||||
input map[string]interface{}
|
||||
expected map[string]interface{}
|
||||
input Params
|
||||
expected Params
|
||||
}{
|
||||
{
|
||||
map[string]interface{}{
|
||||
@@ -47,6 +47,9 @@ func TestToLower(t *testing.T) {
|
||||
"gHi": map[string]interface{}{
|
||||
"J": 25,
|
||||
},
|
||||
"jKl": map[string]string{
|
||||
"M": "26",
|
||||
},
|
||||
},
|
||||
Params{
|
||||
"abc": 32,
|
||||
@@ -60,14 +63,17 @@ func TestToLower(t *testing.T) {
|
||||
"ghi": Params{
|
||||
"j": 25,
|
||||
},
|
||||
"jkl": Params{
|
||||
"m": "26",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprint(i), func(t *testing.T) {
|
||||
// ToLower modifies input.
|
||||
ToLower(test.input)
|
||||
// PrepareParams modifies input.
|
||||
PrepareParams(test.input)
|
||||
if !reflect.DeepEqual(test.expected, test.input) {
|
||||
t.Errorf("[%d] Expected\n%#v, got\n%#v\n", i, test.expected, test.input)
|
||||
}
|
||||
@@ -75,6 +81,39 @@ func TestToLower(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestToSliceStringMap(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
tests := []struct {
|
||||
input interface{}
|
||||
expected []map[string]interface{}
|
||||
}{
|
||||
{
|
||||
input: []map[string]interface{}{
|
||||
{"abc": 123},
|
||||
},
|
||||
expected: []map[string]interface{}{
|
||||
{"abc": 123},
|
||||
},
|
||||
}, {
|
||||
input: []interface{}{
|
||||
map[string]interface{}{
|
||||
"def": 456,
|
||||
},
|
||||
},
|
||||
expected: []map[string]interface{}{
|
||||
{"def": 456},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
v, err := ToSliceStringMap(test.input)
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(v, qt.DeepEquals, test.expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenameKeys(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
@@ -121,5 +160,4 @@ func TestRenameKeys(t *testing.T) {
|
||||
if !reflect.DeepEqual(expected, m) {
|
||||
t.Errorf("Expected\n%#v, got\n%#v\n", expected, m)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+176
-2
@@ -14,6 +14,7 @@
|
||||
package maps
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
@@ -29,6 +30,110 @@ func (p Params) Get(indices ...string) interface{} {
|
||||
return v
|
||||
}
|
||||
|
||||
// Set overwrites values in p with values in pp for common or new keys.
|
||||
// This is done recursively.
|
||||
func (p Params) Set(pp Params) {
|
||||
for k, v := range pp {
|
||||
vv, found := p[k]
|
||||
if !found {
|
||||
p[k] = v
|
||||
} else {
|
||||
switch vvv := vv.(type) {
|
||||
case Params:
|
||||
if pv, ok := v.(Params); ok {
|
||||
vvv.Set(pv)
|
||||
} else {
|
||||
p[k] = v
|
||||
}
|
||||
default:
|
||||
p[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IsZero returns true if p is considered empty.
|
||||
func (p Params) IsZero() bool {
|
||||
if p == nil || len(p) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
if len(p) > 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
for k, _ := range p {
|
||||
return k == mergeStrategyKey
|
||||
}
|
||||
|
||||
return false
|
||||
|
||||
}
|
||||
|
||||
// Merge transfers values from pp to p for new keys.
|
||||
// This is done recursively.
|
||||
func (p Params) Merge(pp Params) {
|
||||
p.merge("", pp)
|
||||
}
|
||||
|
||||
func (p Params) merge(ps ParamsMergeStrategy, pp Params) {
|
||||
ns, found := p.GetMergeStrategy()
|
||||
|
||||
var ms = ns
|
||||
if !found && ps != "" {
|
||||
ms = ps
|
||||
}
|
||||
|
||||
noUpdate := ms == ParamsMergeStrategyNone
|
||||
noUpdate = noUpdate || (ps != "" && ps == ParamsMergeStrategyShallow)
|
||||
|
||||
for k, v := range pp {
|
||||
|
||||
if k == mergeStrategyKey {
|
||||
continue
|
||||
}
|
||||
vv, found := p[k]
|
||||
|
||||
if found {
|
||||
// Key matches, if both sides are Params, we try to merge.
|
||||
if vvv, ok := vv.(Params); ok {
|
||||
if pv, ok := v.(Params); ok {
|
||||
vvv.merge(ms, pv)
|
||||
}
|
||||
}
|
||||
} else if !noUpdate {
|
||||
p[k] = v
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (p Params) GetMergeStrategy() (ParamsMergeStrategy, bool) {
|
||||
if v, found := p[mergeStrategyKey]; found {
|
||||
if s, ok := v.(ParamsMergeStrategy); ok {
|
||||
return s, true
|
||||
}
|
||||
}
|
||||
return ParamsMergeStrategyShallow, false
|
||||
}
|
||||
|
||||
func (p Params) DeleteMergeStrategy() bool {
|
||||
if _, found := p[mergeStrategyKey]; found {
|
||||
delete(p, mergeStrategyKey)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (p Params) SetDefaultMergeStrategy(s ParamsMergeStrategy) {
|
||||
switch s {
|
||||
case ParamsMergeStrategyDeep, ParamsMergeStrategyNone, ParamsMergeStrategyShallow:
|
||||
default:
|
||||
panic(fmt.Sprintf("invalid merge strategy %q", s))
|
||||
}
|
||||
p[mergeStrategyKey] = s
|
||||
}
|
||||
|
||||
func getNested(m map[string]interface{}, indices []string) (interface{}, string, map[string]interface{}) {
|
||||
if len(indices) == 0 {
|
||||
return nil, "", nil
|
||||
@@ -37,7 +142,11 @@ func getNested(m map[string]interface{}, indices []string) (interface{}, string,
|
||||
first := indices[0]
|
||||
v, found := m[strings.ToLower(cast.ToString(first))]
|
||||
if !found {
|
||||
if len(indices) == 1 {
|
||||
return nil, first, m
|
||||
}
|
||||
return nil, "", nil
|
||||
|
||||
}
|
||||
|
||||
if len(indices) == 1 {
|
||||
@@ -76,11 +185,10 @@ func GetNestedParam(keyStr, separator string, candidates ...Params) (interface{}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
|
||||
}
|
||||
|
||||
func GetNestedParamFn(keyStr, separator string, lookupFn func(key string) interface{}) (interface{}, string, map[string]interface{}, error) {
|
||||
keySegments := strings.Split(strings.ToLower(keyStr), separator)
|
||||
keySegments := strings.Split(keyStr, separator)
|
||||
if len(keySegments) == 0 {
|
||||
return nil, "", nil, nil
|
||||
}
|
||||
@@ -105,3 +213,69 @@ func GetNestedParamFn(keyStr, separator string, lookupFn func(key string) interf
|
||||
|
||||
return nil, "", nil, nil
|
||||
}
|
||||
|
||||
// ParamsMergeStrategy tells what strategy to use in Params.Merge.
|
||||
type ParamsMergeStrategy string
|
||||
|
||||
const (
|
||||
// Do not merge.
|
||||
ParamsMergeStrategyNone ParamsMergeStrategy = "none"
|
||||
// Only add new keys.
|
||||
ParamsMergeStrategyShallow ParamsMergeStrategy = "shallow"
|
||||
// Add new keys, merge existing.
|
||||
ParamsMergeStrategyDeep ParamsMergeStrategy = "deep"
|
||||
|
||||
mergeStrategyKey = "_merge"
|
||||
)
|
||||
|
||||
func toMergeStrategy(v interface{}) ParamsMergeStrategy {
|
||||
s := ParamsMergeStrategy(cast.ToString(v))
|
||||
switch s {
|
||||
case ParamsMergeStrategyDeep, ParamsMergeStrategyNone, ParamsMergeStrategyShallow:
|
||||
return s
|
||||
default:
|
||||
return ParamsMergeStrategyDeep
|
||||
}
|
||||
}
|
||||
|
||||
// PrepareParams
|
||||
// * makes all the keys in the given map lower cased and will do so
|
||||
// * This will modify the map given.
|
||||
// * Any nested map[interface{}]interface{}, map[string]interface{},map[string]string will be converted to Params.
|
||||
// * Any _merge value will be converted to proper type and value.
|
||||
func PrepareParams(m Params) {
|
||||
for k, v := range m {
|
||||
var retyped bool
|
||||
lKey := strings.ToLower(k)
|
||||
if lKey == mergeStrategyKey {
|
||||
v = toMergeStrategy(v)
|
||||
retyped = true
|
||||
} else {
|
||||
switch vv := v.(type) {
|
||||
case map[interface{}]interface{}:
|
||||
var p Params = cast.ToStringMap(v)
|
||||
v = p
|
||||
PrepareParams(p)
|
||||
retyped = true
|
||||
case map[string]interface{}:
|
||||
var p Params = v.(map[string]interface{})
|
||||
v = p
|
||||
PrepareParams(p)
|
||||
retyped = true
|
||||
case map[string]string:
|
||||
p := make(Params)
|
||||
for k, v := range vv {
|
||||
p[k] = v
|
||||
}
|
||||
v = p
|
||||
PrepareParams(p)
|
||||
retyped = true
|
||||
}
|
||||
}
|
||||
|
||||
if retyped || k != lKey {
|
||||
delete(m, k)
|
||||
m[lKey] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+120
-1
@@ -20,7 +20,6 @@ import (
|
||||
)
|
||||
|
||||
func TestGetNestedParam(t *testing.T) {
|
||||
|
||||
m := map[string]interface{}{
|
||||
"string": "value",
|
||||
"first": 1,
|
||||
@@ -47,5 +46,125 @@ func TestGetNestedParam(t *testing.T) {
|
||||
c.Assert(must("nested_color", "_", m), qt.Equals, "blue")
|
||||
c.Assert(must("nested.nestednested.color", ".", m), qt.Equals, "green")
|
||||
c.Assert(must("string.name", ".", m), qt.IsNil)
|
||||
c.Assert(must("nested.foo", ".", m), qt.IsNil)
|
||||
}
|
||||
|
||||
// https://github.com/gohugoio/hugo/issues/7903
|
||||
func TestGetNestedParamFnNestedNewKey(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
nested := map[string]interface{}{
|
||||
"color": "blue",
|
||||
}
|
||||
m := map[string]interface{}{
|
||||
"nested": nested,
|
||||
}
|
||||
|
||||
existing, nestedKey, owner, err := GetNestedParamFn("nested.new", ".", func(key string) interface{} {
|
||||
return m[key]
|
||||
})
|
||||
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(existing, qt.IsNil)
|
||||
c.Assert(nestedKey, qt.Equals, "new")
|
||||
c.Assert(owner, qt.DeepEquals, nested)
|
||||
}
|
||||
|
||||
func TestParamsSetAndMerge(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
createParamsPair := func() (Params, Params) {
|
||||
p1 := Params{"a": "av", "c": "cv", "nested": Params{"al2": "al2v", "cl2": "cl2v"}}
|
||||
p2 := Params{"b": "bv", "a": "abv", "nested": Params{"bl2": "bl2v", "al2": "al2bv"}, mergeStrategyKey: ParamsMergeStrategyDeep}
|
||||
return p1, p2
|
||||
}
|
||||
|
||||
p1, p2 := createParamsPair()
|
||||
|
||||
p1.Set(p2)
|
||||
|
||||
c.Assert(p1, qt.DeepEquals, Params{
|
||||
"a": "abv",
|
||||
"c": "cv",
|
||||
"nested": Params{
|
||||
"al2": "al2bv",
|
||||
"cl2": "cl2v",
|
||||
"bl2": "bl2v",
|
||||
},
|
||||
"b": "bv",
|
||||
mergeStrategyKey: ParamsMergeStrategyDeep,
|
||||
})
|
||||
|
||||
p1, p2 = createParamsPair()
|
||||
|
||||
p1.Merge(p2)
|
||||
|
||||
// Default is to do a shallow merge.
|
||||
c.Assert(p1, qt.DeepEquals, Params{
|
||||
"c": "cv",
|
||||
"nested": Params{
|
||||
"al2": "al2v",
|
||||
"cl2": "cl2v",
|
||||
},
|
||||
"b": "bv",
|
||||
"a": "av",
|
||||
})
|
||||
|
||||
p1, p2 = createParamsPair()
|
||||
p1.SetDefaultMergeStrategy(ParamsMergeStrategyNone)
|
||||
p1.Merge(p2)
|
||||
p1.DeleteMergeStrategy()
|
||||
|
||||
c.Assert(p1, qt.DeepEquals, Params{
|
||||
"a": "av",
|
||||
"c": "cv",
|
||||
"nested": Params{
|
||||
"al2": "al2v",
|
||||
"cl2": "cl2v",
|
||||
},
|
||||
})
|
||||
|
||||
p1, p2 = createParamsPair()
|
||||
p1.SetDefaultMergeStrategy(ParamsMergeStrategyShallow)
|
||||
p1.Merge(p2)
|
||||
p1.DeleteMergeStrategy()
|
||||
|
||||
c.Assert(p1, qt.DeepEquals, Params{
|
||||
"a": "av",
|
||||
"c": "cv",
|
||||
"nested": Params{
|
||||
"al2": "al2v",
|
||||
"cl2": "cl2v",
|
||||
},
|
||||
"b": "bv",
|
||||
})
|
||||
|
||||
p1, p2 = createParamsPair()
|
||||
p1.SetDefaultMergeStrategy(ParamsMergeStrategyDeep)
|
||||
p1.Merge(p2)
|
||||
p1.DeleteMergeStrategy()
|
||||
|
||||
c.Assert(p1, qt.DeepEquals, Params{
|
||||
"nested": Params{
|
||||
"al2": "al2v",
|
||||
"cl2": "cl2v",
|
||||
"bl2": "bl2v",
|
||||
},
|
||||
"b": "bv",
|
||||
"a": "av",
|
||||
"c": "cv",
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func TestParamsIsZero(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
var nilParams Params
|
||||
|
||||
c.Assert(Params{}.IsZero(), qt.IsTrue)
|
||||
c.Assert(nilParams.IsZero(), qt.IsTrue)
|
||||
c.Assert(Params{"foo": "bar"}.IsZero(), qt.IsFalse)
|
||||
c.Assert(Params{"_merge": "foo", "foo": "bar"}.IsZero(), qt.IsFalse)
|
||||
c.Assert(Params{"_merge": "foo"}.IsZero(), qt.IsTrue)
|
||||
}
|
||||
|
||||
+21
-2
@@ -51,7 +51,6 @@ func NewScratcher() Scratcher {
|
||||
//
|
||||
// If the first add for a key is an array or slice, then the next value(s) will be appended.
|
||||
func (c *Scratch) Add(key string, newAddend interface{}) (string, error) {
|
||||
|
||||
var newVal interface{}
|
||||
c.mu.RLock()
|
||||
existingAddend, found := c.values[key]
|
||||
@@ -107,6 +106,15 @@ func (c *Scratch) Get(key string) interface{} {
|
||||
return val
|
||||
}
|
||||
|
||||
// Values returns the raw backing map. Note that you should just use
|
||||
// this method on the locally scoped Scratch instances you obtain via newScratch, not
|
||||
// .Page.Scratch etc., as that will lead to concurrency issues.
|
||||
func (c *Scratch) Values() map[string]interface{} {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.values
|
||||
}
|
||||
|
||||
// SetInMap stores a value to a map with the given key in the Node context.
|
||||
// This map can later be retrieved with GetSortedMapValues.
|
||||
func (c *Scratch) SetInMap(key string, mapKey string, value interface{}) string {
|
||||
@@ -121,6 +129,17 @@ func (c *Scratch) SetInMap(key string, mapKey string, value interface{}) string
|
||||
return ""
|
||||
}
|
||||
|
||||
// DeleteInMap deletes a value to a map with the given key in the Node context.
|
||||
func (c *Scratch) DeleteInMap(key string, mapKey string) string {
|
||||
c.mu.Lock()
|
||||
_, found := c.values[key]
|
||||
if found {
|
||||
delete(c.values[key].(map[string]interface{}), mapKey)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetSortedMapValues returns a sorted map previously filled with SetInMap.
|
||||
func (c *Scratch) GetSortedMapValues(key string) interface{} {
|
||||
c.mu.RLock()
|
||||
@@ -147,7 +166,7 @@ func (c *Scratch) GetSortedMapValues(key string) interface{} {
|
||||
return sortedArray
|
||||
}
|
||||
|
||||
// NewScratch returns a new instance Scratch.
|
||||
// NewScratch returns a new instance of Scratch.
|
||||
func NewScratch() *Scratch {
|
||||
return &Scratch{values: make(map[string]interface{})}
|
||||
}
|
||||
|
||||
@@ -47,10 +47,12 @@ func TestScratchAdd(t *testing.T) {
|
||||
scratch.Add("scratch", scratch)
|
||||
_, err := scratch.Add("scratch", scratch)
|
||||
|
||||
m := scratch.Values()
|
||||
c.Assert(m, qt.HasLen, 5)
|
||||
|
||||
if err == nil {
|
||||
t.Errorf("Expected error from invalid arithmetic")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestScratchAddSlice(t *testing.T) {
|
||||
@@ -93,7 +95,6 @@ func TestScratchAddTypedSliceToInterfaceSlice(t *testing.T) {
|
||||
_, err := scratch.Add("slice", []int{1, 2})
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(scratch.Get("slice"), qt.DeepEquals, []int{1, 2})
|
||||
|
||||
}
|
||||
|
||||
// https://github.com/gohugoio/hugo/issues/5361
|
||||
@@ -107,7 +108,6 @@ func TestScratchAddDifferentTypedSliceToInterfaceSlice(t *testing.T) {
|
||||
_, err := scratch.Add("slice", []int{1, 2})
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(scratch.Get("slice"), qt.DeepEquals, []interface{}{"foo", 1, 2})
|
||||
|
||||
}
|
||||
|
||||
func TestScratchSet(t *testing.T) {
|
||||
@@ -188,6 +188,20 @@ func TestScratchSetInMap(t *testing.T) {
|
||||
c.Assert(scratch.GetSortedMapValues("key"), qt.DeepEquals, []interface{}{0: "Abc (updated)", 1: "Def", 2: "Lux", 3: "Zyx"})
|
||||
}
|
||||
|
||||
func TestScratchDeleteInMap(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
scratch := NewScratch()
|
||||
scratch.SetInMap("key", "lux", "Lux")
|
||||
scratch.SetInMap("key", "abc", "Abc")
|
||||
scratch.SetInMap("key", "zyx", "Zyx")
|
||||
scratch.DeleteInMap("key", "abc")
|
||||
scratch.SetInMap("key", "def", "Def")
|
||||
scratch.DeleteInMap("key", "lmn") // Do nothing
|
||||
c.Assert(scratch.GetSortedMapValues("key"), qt.DeepEquals, []interface{}{0: "Def", 1: "Lux", 2: "Zyx"})
|
||||
}
|
||||
|
||||
func TestScratchGetSortedMapValues(t *testing.T) {
|
||||
t.Parallel()
|
||||
scratch := NewScratch()
|
||||
|
||||
@@ -16,13 +16,14 @@ package para
|
||||
import (
|
||||
"context"
|
||||
"runtime"
|
||||
|
||||
"sort"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/htesting"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
@@ -31,6 +32,10 @@ func TestPara(t *testing.T) {
|
||||
t.Skipf("skip para test, CPU count is %d", runtime.NumCPU())
|
||||
}
|
||||
|
||||
if !htesting.IsCI() {
|
||||
t.Skip("skip para test when not running on CI")
|
||||
}
|
||||
|
||||
c := qt.New(t)
|
||||
|
||||
c.Run("Order", func(c *qt.C) {
|
||||
@@ -60,7 +65,6 @@ func TestPara(t *testing.T) {
|
||||
c.Assert(sort.IntsAreSorted(result), qt.Equals, false, qt.Commentf("Para does not seem to be parallel"))
|
||||
sort.Ints(result)
|
||||
c.Assert(result, qt.DeepEquals, ints)
|
||||
|
||||
})
|
||||
|
||||
c.Run("Time", func(c *qt.C) {
|
||||
@@ -83,8 +87,9 @@ func TestPara(t *testing.T) {
|
||||
|
||||
c.Assert(r.Wait(), qt.IsNil)
|
||||
c.Assert(counter, qt.Equals, int64(n))
|
||||
c.Assert(time.Since(start) < n/2*time.Millisecond, qt.Equals, true)
|
||||
|
||||
since := time.Since(start)
|
||||
limit := n / 2 * time.Millisecond
|
||||
c.Assert(since < limit, qt.Equals, true, qt.Commentf("%s >= %s", since, limit))
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
// Copyright 2021 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package paths
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FilePathSeparator as defined by os.Separator.
|
||||
const FilePathSeparator = string(filepath.Separator)
|
||||
|
||||
// filepathPathBridge is a bridge for common functionality in filepath vs path
|
||||
type filepathPathBridge interface {
|
||||
Base(in string) string
|
||||
Clean(in string) string
|
||||
Dir(in string) string
|
||||
Ext(in string) string
|
||||
Join(elem ...string) string
|
||||
Separator() string
|
||||
}
|
||||
|
||||
type filepathBridge struct {
|
||||
}
|
||||
|
||||
func (filepathBridge) Base(in string) string {
|
||||
return filepath.Base(in)
|
||||
}
|
||||
|
||||
func (filepathBridge) Clean(in string) string {
|
||||
return filepath.Clean(in)
|
||||
}
|
||||
|
||||
func (filepathBridge) Dir(in string) string {
|
||||
return filepath.Dir(in)
|
||||
}
|
||||
|
||||
func (filepathBridge) Ext(in string) string {
|
||||
return filepath.Ext(in)
|
||||
}
|
||||
|
||||
func (filepathBridge) Join(elem ...string) string {
|
||||
return filepath.Join(elem...)
|
||||
}
|
||||
|
||||
func (filepathBridge) Separator() string {
|
||||
return FilePathSeparator
|
||||
}
|
||||
|
||||
var fpb filepathBridge
|
||||
|
||||
// ToSlashTrimLeading is just a filepath.ToSlash with an added / prefix trimmer.
|
||||
func ToSlashTrimLeading(s string) string {
|
||||
return strings.TrimPrefix(filepath.ToSlash(s), "/")
|
||||
}
|
||||
|
||||
// MakeTitle converts the path given to a suitable title, trimming whitespace
|
||||
// and replacing hyphens with whitespace.
|
||||
func MakeTitle(inpath string) string {
|
||||
return strings.Replace(strings.TrimSpace(inpath), "-", " ", -1)
|
||||
}
|
||||
|
||||
// ReplaceExtension takes a path and an extension, strips the old extension
|
||||
// and returns the path with the new extension.
|
||||
func ReplaceExtension(path string, newExt string) string {
|
||||
f, _ := fileAndExt(path, fpb)
|
||||
return f + "." + newExt
|
||||
}
|
||||
|
||||
func makePathRelative(inPath string, possibleDirectories ...string) (string, error) {
|
||||
for _, currentPath := range possibleDirectories {
|
||||
if strings.HasPrefix(inPath, currentPath) {
|
||||
return strings.TrimPrefix(inPath, currentPath), nil
|
||||
}
|
||||
}
|
||||
return inPath, errors.New("can't extract relative path, unknown prefix")
|
||||
}
|
||||
|
||||
// Should be good enough for Hugo.
|
||||
var isFileRe = regexp.MustCompile(`.*\..{1,6}$`)
|
||||
|
||||
// GetDottedRelativePath expects a relative path starting after the content directory.
|
||||
// It returns a relative path with dots ("..") navigating up the path structure.
|
||||
func GetDottedRelativePath(inPath string) string {
|
||||
inPath = filepath.Clean(filepath.FromSlash(inPath))
|
||||
|
||||
if inPath == "." {
|
||||
return "./"
|
||||
}
|
||||
|
||||
if !isFileRe.MatchString(inPath) && !strings.HasSuffix(inPath, FilePathSeparator) {
|
||||
inPath += FilePathSeparator
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(inPath, FilePathSeparator) {
|
||||
inPath = FilePathSeparator + inPath
|
||||
}
|
||||
|
||||
dir, _ := filepath.Split(inPath)
|
||||
|
||||
sectionCount := strings.Count(dir, FilePathSeparator)
|
||||
|
||||
if sectionCount == 0 || dir == FilePathSeparator {
|
||||
return "./"
|
||||
}
|
||||
|
||||
var dottedPath string
|
||||
|
||||
for i := 1; i < sectionCount; i++ {
|
||||
dottedPath += "../"
|
||||
}
|
||||
|
||||
return dottedPath
|
||||
}
|
||||
|
||||
// ExtNoDelimiter takes a path and returns the extension, excluding the delimiter, i.e. "md".
|
||||
func ExtNoDelimiter(in string) string {
|
||||
return strings.TrimPrefix(Ext(in), ".")
|
||||
}
|
||||
|
||||
// Ext takes a path and returns the extension, including the delimiter, i.e. ".md".
|
||||
func Ext(in string) string {
|
||||
_, ext := fileAndExt(in, fpb)
|
||||
return ext
|
||||
}
|
||||
|
||||
// PathAndExt is the same as FileAndExt, but it uses the path package.
|
||||
func PathAndExt(in string) (string, string) {
|
||||
return fileAndExt(in, pb)
|
||||
}
|
||||
|
||||
// FileAndExt takes a path and returns the file and extension separated,
|
||||
// the extension including the delimiter, i.e. ".md".
|
||||
func FileAndExt(in string) (string, string) {
|
||||
return fileAndExt(in, fpb)
|
||||
}
|
||||
|
||||
// FileAndExtNoDelimiter takes a path and returns the file and extension separated,
|
||||
// the extension excluding the delimiter, e.g "md".
|
||||
func FileAndExtNoDelimiter(in string) (string, string) {
|
||||
file, ext := fileAndExt(in, fpb)
|
||||
return file, strings.TrimPrefix(ext, ".")
|
||||
}
|
||||
|
||||
// Filename takes a file path, strips out the extension,
|
||||
// and returns the name of the file.
|
||||
func Filename(in string) (name string) {
|
||||
name, _ = fileAndExt(in, fpb)
|
||||
return
|
||||
}
|
||||
|
||||
// PathNoExt takes a path, strips out the extension,
|
||||
// and returns the name of the file.
|
||||
func PathNoExt(in string) string {
|
||||
return strings.TrimSuffix(in, path.Ext(in))
|
||||
}
|
||||
|
||||
// FileAndExt returns the filename and any extension of a file path as
|
||||
// two separate strings.
|
||||
//
|
||||
// If the path, in, contains a directory name ending in a slash,
|
||||
// then both name and ext will be empty strings.
|
||||
//
|
||||
// If the path, in, is either the current directory, the parent
|
||||
// directory or the root directory, or an empty string,
|
||||
// then both name and ext will be empty strings.
|
||||
//
|
||||
// If the path, in, represents the path of a file without an extension,
|
||||
// then name will be the name of the file and ext will be an empty string.
|
||||
//
|
||||
// If the path, in, represents a filename with an extension,
|
||||
// then name will be the filename minus any extension - including the dot
|
||||
// and ext will contain the extension - minus the dot.
|
||||
func fileAndExt(in string, b filepathPathBridge) (name string, ext string) {
|
||||
ext = b.Ext(in)
|
||||
base := b.Base(in)
|
||||
|
||||
return extractFilename(in, ext, base, b.Separator()), ext
|
||||
}
|
||||
|
||||
func extractFilename(in, ext, base, pathSeparator string) (name string) {
|
||||
// No file name cases. These are defined as:
|
||||
// 1. any "in" path that ends in a pathSeparator
|
||||
// 2. any "base" consisting of just an pathSeparator
|
||||
// 3. any "base" consisting of just an empty string
|
||||
// 4. any "base" consisting of just the current directory i.e. "."
|
||||
// 5. any "base" consisting of just the parent directory i.e. ".."
|
||||
if (strings.LastIndex(in, pathSeparator) == len(in)-1) || base == "" || base == "." || base == ".." || base == pathSeparator {
|
||||
name = "" // there is NO filename
|
||||
} else if ext != "" { // there was an Extension
|
||||
// return the filename minus the extension (and the ".")
|
||||
name = base[:strings.LastIndex(base, ".")]
|
||||
} else {
|
||||
// no extension case so just return base, which willi
|
||||
// be the filename
|
||||
name = base
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetRelativePath returns the relative path of a given path.
|
||||
func GetRelativePath(path, base string) (final string, err error) {
|
||||
if filepath.IsAbs(path) && base == "" {
|
||||
return "", errors.New("source: missing base directory")
|
||||
}
|
||||
name := filepath.Clean(path)
|
||||
base = filepath.Clean(base)
|
||||
|
||||
name, err = filepath.Rel(base, name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if strings.HasSuffix(filepath.FromSlash(path), FilePathSeparator) && !strings.HasSuffix(name, FilePathSeparator) {
|
||||
name += FilePathSeparator
|
||||
}
|
||||
return name, nil
|
||||
}
|
||||
|
||||
// PathPrep prepares the path using the uglify setting to create paths on
|
||||
// either the form /section/name/index.html or /section/name.html.
|
||||
func PathPrep(ugly bool, in string) string {
|
||||
if ugly {
|
||||
return Uglify(in)
|
||||
}
|
||||
return PrettifyPath(in)
|
||||
}
|
||||
|
||||
// PrettifyPath is the same as PrettifyURLPath but for file paths.
|
||||
// /section/name.html becomes /section/name/index.html
|
||||
// /section/name/ becomes /section/name/index.html
|
||||
// /section/name/index.html becomes /section/name/index.html
|
||||
func PrettifyPath(in string) string {
|
||||
return prettifyPath(in, fpb)
|
||||
}
|
||||
|
||||
func prettifyPath(in string, b filepathPathBridge) string {
|
||||
if filepath.Ext(in) == "" {
|
||||
// /section/name/ -> /section/name/index.html
|
||||
if len(in) < 2 {
|
||||
return b.Separator()
|
||||
}
|
||||
return b.Join(in, "index.html")
|
||||
}
|
||||
name, ext := fileAndExt(in, b)
|
||||
if name == "index" {
|
||||
// /section/name/index.html -> /section/name/index.html
|
||||
return b.Clean(in)
|
||||
}
|
||||
// /section/name.html -> /section/name/index.html
|
||||
return b.Join(b.Dir(in), name, "index"+ext)
|
||||
}
|
||||
|
||||
type NamedSlice struct {
|
||||
Name string
|
||||
Slice []string
|
||||
}
|
||||
|
||||
func (n NamedSlice) String() string {
|
||||
if len(n.Slice) == 0 {
|
||||
return n.Name
|
||||
}
|
||||
return fmt.Sprintf("%s%s{%s}", n.Name, FilePathSeparator, strings.Join(n.Slice, ","))
|
||||
}
|
||||
|
||||
// FindCWD returns the current working directory from where the Hugo
|
||||
// executable is run.
|
||||
func FindCWD() (string, error) {
|
||||
serverFile, err := filepath.Abs(os.Args[0])
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("can't get absolute path for executable: %v", err)
|
||||
}
|
||||
|
||||
path := filepath.Dir(serverFile)
|
||||
realFile, err := filepath.EvalSymlinks(serverFile)
|
||||
if err != nil {
|
||||
if _, err = os.Stat(serverFile + ".exe"); err == nil {
|
||||
realFile = filepath.Clean(serverFile + ".exe")
|
||||
}
|
||||
}
|
||||
|
||||
if err == nil && realFile != serverFile {
|
||||
path = filepath.Dir(realFile)
|
||||
}
|
||||
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// AddTrailingSlash adds a trailing Unix styled slash (/) if not already
|
||||
// there.
|
||||
func AddTrailingSlash(path string) string {
|
||||
if !strings.HasSuffix(path, "/") {
|
||||
path += "/"
|
||||
}
|
||||
return path
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
// Copyright 2021 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package paths
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestGetRelativePath(t *testing.T) {
|
||||
tests := []struct {
|
||||
path string
|
||||
base string
|
||||
expect interface{}
|
||||
}{
|
||||
{filepath.FromSlash("/a/b"), filepath.FromSlash("/a"), filepath.FromSlash("b")},
|
||||
{filepath.FromSlash("/a/b/c/"), filepath.FromSlash("/a"), filepath.FromSlash("b/c/")},
|
||||
{filepath.FromSlash("/c"), filepath.FromSlash("/a/b"), filepath.FromSlash("../../c")},
|
||||
{filepath.FromSlash("/c"), "", false},
|
||||
}
|
||||
for i, this := range tests {
|
||||
// ultimately a fancy wrapper around filepath.Rel
|
||||
result, err := GetRelativePath(this.path, this.base)
|
||||
|
||||
if b, ok := this.expect.(bool); ok && !b {
|
||||
if err == nil {
|
||||
t.Errorf("[%d] GetRelativePath didn't return an expected error", i)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("[%d] GetRelativePath failed: %s", i, err)
|
||||
continue
|
||||
}
|
||||
if result != this.expect {
|
||||
t.Errorf("[%d] GetRelativePath got %v but expected %v", i, result, this.expect)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakePathRelative(t *testing.T) {
|
||||
type test struct {
|
||||
inPath, path1, path2, output string
|
||||
}
|
||||
|
||||
data := []test{
|
||||
{"/abc/bcd/ab.css", "/abc/bcd", "/bbc/bcd", "/ab.css"},
|
||||
{"/abc/bcd/ab.css", "/abcd/bcd", "/abc/bcd", "/ab.css"},
|
||||
}
|
||||
|
||||
for i, d := range data {
|
||||
output, _ := makePathRelative(d.inPath, d.path1, d.path2)
|
||||
if d.output != output {
|
||||
t.Errorf("Test #%d failed. Expected %q got %q", i, d.output, output)
|
||||
}
|
||||
}
|
||||
_, error := makePathRelative("a/b/c.ss", "/a/c", "/d/c", "/e/f")
|
||||
|
||||
if error == nil {
|
||||
t.Errorf("Test failed, expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDottedRelativePath(t *testing.T) {
|
||||
// on Windows this will receive both kinds, both country and western ...
|
||||
for _, f := range []func(string) string{filepath.FromSlash, func(s string) string { return s }} {
|
||||
doTestGetDottedRelativePath(f, t)
|
||||
}
|
||||
}
|
||||
|
||||
func doTestGetDottedRelativePath(urlFixer func(string) string, t *testing.T) {
|
||||
type test struct {
|
||||
input, expected string
|
||||
}
|
||||
data := []test{
|
||||
{"", "./"},
|
||||
{urlFixer("/"), "./"},
|
||||
{urlFixer("post"), "../"},
|
||||
{urlFixer("/post"), "../"},
|
||||
{urlFixer("post/"), "../"},
|
||||
{urlFixer("tags/foo.html"), "../"},
|
||||
{urlFixer("/tags/foo.html"), "../"},
|
||||
{urlFixer("/post/"), "../"},
|
||||
{urlFixer("////post/////"), "../"},
|
||||
{urlFixer("/foo/bar/index.html"), "../../"},
|
||||
{urlFixer("/foo/bar/foo/"), "../../../"},
|
||||
{urlFixer("/foo/bar/foo"), "../../../"},
|
||||
{urlFixer("foo/bar/foo/"), "../../../"},
|
||||
{urlFixer("foo/bar/foo/bar"), "../../../../"},
|
||||
{"404.html", "./"},
|
||||
{"404.xml", "./"},
|
||||
{"/404.html", "./"},
|
||||
}
|
||||
for i, d := range data {
|
||||
output := GetDottedRelativePath(d.input)
|
||||
if d.expected != output {
|
||||
t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeTitle(t *testing.T) {
|
||||
type test struct {
|
||||
input, expected string
|
||||
}
|
||||
data := []test{
|
||||
{"Make-Title", "Make Title"},
|
||||
{"MakeTitle", "MakeTitle"},
|
||||
{"make_title", "make_title"},
|
||||
}
|
||||
for i, d := range data {
|
||||
output := MakeTitle(d.input)
|
||||
if d.expected != output {
|
||||
t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Replace Extension is probably poorly named, but the intent of the
|
||||
// function is to accept a path and return only the file name with a
|
||||
// new extension. It's intentionally designed to strip out the path
|
||||
// and only provide the name. We should probably rename the function to
|
||||
// be more explicit at some point.
|
||||
func TestReplaceExtension(t *testing.T) {
|
||||
type test struct {
|
||||
input, newext, expected string
|
||||
}
|
||||
data := []test{
|
||||
// These work according to the above definition
|
||||
{"/some/random/path/file.xml", "html", "file.html"},
|
||||
{"/banana.html", "xml", "banana.xml"},
|
||||
{"./banana.html", "xml", "banana.xml"},
|
||||
{"banana/pie/index.html", "xml", "index.xml"},
|
||||
{"../pies/fish/index.html", "xml", "index.xml"},
|
||||
// but these all fail
|
||||
{"filename-without-an-ext", "ext", "filename-without-an-ext.ext"},
|
||||
{"/filename-without-an-ext", "ext", "filename-without-an-ext.ext"},
|
||||
{"/directory/mydir/", "ext", ".ext"},
|
||||
{"mydir/", "ext", ".ext"},
|
||||
}
|
||||
|
||||
for i, d := range data {
|
||||
output := ReplaceExtension(filepath.FromSlash(d.input), d.newext)
|
||||
if d.expected != output {
|
||||
t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtNoDelimiter(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
c.Assert(ExtNoDelimiter(filepath.FromSlash("/my/data.json")), qt.Equals, "json")
|
||||
}
|
||||
|
||||
func TestFilename(t *testing.T) {
|
||||
type test struct {
|
||||
input, expected string
|
||||
}
|
||||
data := []test{
|
||||
{"index.html", "index"},
|
||||
{"./index.html", "index"},
|
||||
{"/index.html", "index"},
|
||||
{"index", "index"},
|
||||
{"/tmp/index.html", "index"},
|
||||
{"./filename-no-ext", "filename-no-ext"},
|
||||
{"/filename-no-ext", "filename-no-ext"},
|
||||
{"filename-no-ext", "filename-no-ext"},
|
||||
{"directory/", ""}, // no filename case??
|
||||
{"directory/.hidden.ext", ".hidden"},
|
||||
{"./directory/../~/banana/gold.fish", "gold"},
|
||||
{"../directory/banana.man", "banana"},
|
||||
{"~/mydir/filename.ext", "filename"},
|
||||
{"./directory//tmp/filename.ext", "filename"},
|
||||
}
|
||||
|
||||
for i, d := range data {
|
||||
output := Filename(filepath.FromSlash(d.input))
|
||||
if d.expected != output {
|
||||
t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileAndExt(t *testing.T) {
|
||||
type test struct {
|
||||
input, expectedFile, expectedExt string
|
||||
}
|
||||
data := []test{
|
||||
{"index.html", "index", ".html"},
|
||||
{"./index.html", "index", ".html"},
|
||||
{"/index.html", "index", ".html"},
|
||||
{"index", "index", ""},
|
||||
{"/tmp/index.html", "index", ".html"},
|
||||
{"./filename-no-ext", "filename-no-ext", ""},
|
||||
{"/filename-no-ext", "filename-no-ext", ""},
|
||||
{"filename-no-ext", "filename-no-ext", ""},
|
||||
{"directory/", "", ""}, // no filename case??
|
||||
{"directory/.hidden.ext", ".hidden", ".ext"},
|
||||
{"./directory/../~/banana/gold.fish", "gold", ".fish"},
|
||||
{"../directory/banana.man", "banana", ".man"},
|
||||
{"~/mydir/filename.ext", "filename", ".ext"},
|
||||
{"./directory//tmp/filename.ext", "filename", ".ext"},
|
||||
}
|
||||
|
||||
for i, d := range data {
|
||||
file, ext := fileAndExt(filepath.FromSlash(d.input), fpb)
|
||||
if d.expectedFile != file {
|
||||
t.Errorf("Test %d failed. Expected filename %q got %q.", i, d.expectedFile, file)
|
||||
}
|
||||
if d.expectedExt != ext {
|
||||
t.Errorf("Test %d failed. Expected extension %q got %q.", i, d.expectedExt, ext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindCWD(t *testing.T) {
|
||||
type test struct {
|
||||
expectedDir string
|
||||
expectedErr error
|
||||
}
|
||||
|
||||
// cwd, _ := os.Getwd()
|
||||
data := []test{
|
||||
//{cwd, nil},
|
||||
// Commenting this out. It doesn't work properly.
|
||||
// There's a good reason why we don't use os.Getwd(), it doesn't actually work the way we want it to.
|
||||
// I really don't know a better way to test this function. - SPF 2014.11.04
|
||||
}
|
||||
for i, d := range data {
|
||||
dir, err := FindCWD()
|
||||
if d.expectedDir != dir {
|
||||
t.Errorf("Test %d failed. Expected %q but got %q", i, d.expectedDir, dir)
|
||||
}
|
||||
if d.expectedErr != err {
|
||||
t.Errorf("Test %d failed. Expected %q but got %q", i, d.expectedErr, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
// Copyright 2021 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package paths
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/PuerkitoBio/purell"
|
||||
)
|
||||
|
||||
type pathBridge struct {
|
||||
}
|
||||
|
||||
func (pathBridge) Base(in string) string {
|
||||
return path.Base(in)
|
||||
}
|
||||
|
||||
func (pathBridge) Clean(in string) string {
|
||||
return path.Clean(in)
|
||||
}
|
||||
|
||||
func (pathBridge) Dir(in string) string {
|
||||
return path.Dir(in)
|
||||
}
|
||||
|
||||
func (pathBridge) Ext(in string) string {
|
||||
return path.Ext(in)
|
||||
}
|
||||
|
||||
func (pathBridge) Join(elem ...string) string {
|
||||
return path.Join(elem...)
|
||||
}
|
||||
|
||||
func (pathBridge) Separator() string {
|
||||
return "/"
|
||||
}
|
||||
|
||||
var pb pathBridge
|
||||
|
||||
func sanitizeURLWithFlags(in string, f purell.NormalizationFlags) string {
|
||||
s, err := purell.NormalizeURLString(in, f)
|
||||
if err != nil {
|
||||
return in
|
||||
}
|
||||
|
||||
// Temporary workaround for the bug fix and resulting
|
||||
// behavioral change in purell.NormalizeURLString():
|
||||
// a leading '/' was inadvertently added to relative links,
|
||||
// but no longer, see #878.
|
||||
//
|
||||
// I think the real solution is to allow Hugo to
|
||||
// make relative URL with relative path,
|
||||
// e.g. "../../post/hello-again/", as wished by users
|
||||
// in issues #157, #622, etc., without forcing
|
||||
// relative URLs to begin with '/'.
|
||||
// Once the fixes are in, let's remove this kludge
|
||||
// and restore SanitizeURL() to the way it was.
|
||||
// -- @anthonyfok, 2015-02-16
|
||||
//
|
||||
// Begin temporary kludge
|
||||
u, err := url.Parse(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if len(u.Path) > 0 && !strings.HasPrefix(u.Path, "/") {
|
||||
u.Path = "/" + u.Path
|
||||
}
|
||||
return u.String()
|
||||
// End temporary kludge
|
||||
|
||||
// return s
|
||||
|
||||
}
|
||||
|
||||
// SanitizeURL sanitizes the input URL string.
|
||||
func SanitizeURL(in string) string {
|
||||
return sanitizeURLWithFlags(in, purell.FlagsSafe|purell.FlagRemoveTrailingSlash|purell.FlagRemoveDotSegments|purell.FlagRemoveDuplicateSlashes|purell.FlagRemoveUnnecessaryHostDots|purell.FlagRemoveEmptyPortSeparator)
|
||||
}
|
||||
|
||||
// SanitizeURLKeepTrailingSlash is the same as SanitizeURL, but will keep any trailing slash.
|
||||
func SanitizeURLKeepTrailingSlash(in string) string {
|
||||
return sanitizeURLWithFlags(in, purell.FlagsSafe|purell.FlagRemoveDotSegments|purell.FlagRemoveDuplicateSlashes|purell.FlagRemoveUnnecessaryHostDots|purell.FlagRemoveEmptyPortSeparator)
|
||||
}
|
||||
|
||||
// MakePermalink combines base URL with content path to create full URL paths.
|
||||
// Example
|
||||
// base: http://spf13.com/
|
||||
// path: post/how-i-blog
|
||||
// result: http://spf13.com/post/how-i-blog
|
||||
func MakePermalink(host, plink string) *url.URL {
|
||||
base, err := url.Parse(host)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
p, err := url.Parse(plink)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if p.Host != "" {
|
||||
panic(fmt.Errorf("can't make permalink from absolute link %q", plink))
|
||||
}
|
||||
|
||||
base.Path = path.Join(base.Path, p.Path)
|
||||
|
||||
// path.Join will strip off the last /, so put it back if it was there.
|
||||
hadTrailingSlash := (plink == "" && strings.HasSuffix(host, "/")) || strings.HasSuffix(p.Path, "/")
|
||||
if hadTrailingSlash && !strings.HasSuffix(base.Path, "/") {
|
||||
base.Path = base.Path + "/"
|
||||
}
|
||||
|
||||
return base
|
||||
}
|
||||
|
||||
// IsAbsURL determines whether the given path points to an absolute URL.
|
||||
func IsAbsURL(path string) bool {
|
||||
url, err := url.Parse(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return url.IsAbs() || strings.HasPrefix(path, "//")
|
||||
}
|
||||
|
||||
// AddContextRoot adds the context root to an URL if it's not already set.
|
||||
// For relative URL entries on sites with a base url with a context root set (i.e. http://example.com/mysite),
|
||||
// relative URLs must not include the context root if canonifyURLs is enabled. But if it's disabled, it must be set.
|
||||
func AddContextRoot(baseURL, relativePath string) string {
|
||||
url, err := url.Parse(baseURL)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
newPath := path.Join(url.Path, relativePath)
|
||||
|
||||
// path strips trailing slash, ignore root path.
|
||||
if newPath != "/" && strings.HasSuffix(relativePath, "/") {
|
||||
newPath += "/"
|
||||
}
|
||||
return newPath
|
||||
}
|
||||
|
||||
// URLizeAn
|
||||
|
||||
// PrettifyURL takes a URL string and returns a semantic, clean URL.
|
||||
func PrettifyURL(in string) string {
|
||||
x := PrettifyURLPath(in)
|
||||
|
||||
if path.Base(x) == "index.html" {
|
||||
return path.Dir(x)
|
||||
}
|
||||
|
||||
if in == "" {
|
||||
return "/"
|
||||
}
|
||||
|
||||
return x
|
||||
}
|
||||
|
||||
// PrettifyURLPath takes a URL path to a content and converts it
|
||||
// to enable pretty URLs.
|
||||
// /section/name.html becomes /section/name/index.html
|
||||
// /section/name/ becomes /section/name/index.html
|
||||
// /section/name/index.html becomes /section/name/index.html
|
||||
func PrettifyURLPath(in string) string {
|
||||
return prettifyPath(in, pb)
|
||||
}
|
||||
|
||||
// Uglify does the opposite of PrettifyURLPath().
|
||||
// /section/name/index.html becomes /section/name.html
|
||||
// /section/name/ becomes /section/name.html
|
||||
// /section/name.html becomes /section/name.html
|
||||
func Uglify(in string) string {
|
||||
if path.Ext(in) == "" {
|
||||
if len(in) < 2 {
|
||||
return "/"
|
||||
}
|
||||
// /section/name/ -> /section/name.html
|
||||
return path.Clean(in) + ".html"
|
||||
}
|
||||
|
||||
name, ext := fileAndExt(in, pb)
|
||||
if name == "index" {
|
||||
// /section/name/index.html -> /section/name.html
|
||||
d := path.Dir(in)
|
||||
if len(d) > 1 {
|
||||
return d + ext
|
||||
}
|
||||
return in
|
||||
}
|
||||
// /.xml -> /index.xml
|
||||
if name == "" {
|
||||
return path.Dir(in) + "index" + ext
|
||||
}
|
||||
// /section/name.html -> /section/name.html
|
||||
return path.Clean(in)
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
// Copyright 2021 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package paths
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestSanitizeURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"http://foo.bar/", "http://foo.bar"},
|
||||
{"http://foo.bar", "http://foo.bar"}, // issue #1105
|
||||
{"http://foo.bar/zoo/", "http://foo.bar/zoo"}, // issue #931
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
o1 := SanitizeURL(test.input)
|
||||
o2 := SanitizeURLKeepTrailingSlash(test.input)
|
||||
|
||||
expected2 := test.expected
|
||||
|
||||
if strings.HasSuffix(test.input, "/") && !strings.HasSuffix(expected2, "/") {
|
||||
expected2 += "/"
|
||||
}
|
||||
|
||||
if o1 != test.expected {
|
||||
t.Errorf("[%d] 1: Expected %#v, got %#v\n", i, test.expected, o1)
|
||||
}
|
||||
if o2 != expected2 {
|
||||
t.Errorf("[%d] 2: Expected %#v, got %#v\n", i, expected2, o2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakePermalink(t *testing.T) {
|
||||
type test struct {
|
||||
host, link, output string
|
||||
}
|
||||
|
||||
data := []test{
|
||||
{"http://abc.com/foo", "post/bar", "http://abc.com/foo/post/bar"},
|
||||
{"http://abc.com/foo/", "post/bar", "http://abc.com/foo/post/bar"},
|
||||
{"http://abc.com", "post/bar", "http://abc.com/post/bar"},
|
||||
{"http://abc.com", "bar", "http://abc.com/bar"},
|
||||
{"http://abc.com/foo/bar", "post/bar", "http://abc.com/foo/bar/post/bar"},
|
||||
{"http://abc.com/foo/bar", "post/bar/", "http://abc.com/foo/bar/post/bar/"},
|
||||
}
|
||||
|
||||
for i, d := range data {
|
||||
output := MakePermalink(d.host, d.link).String()
|
||||
if d.output != output {
|
||||
t.Errorf("Test #%d failed. Expected %q got %q", i, d.output, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddContextRoot(t *testing.T) {
|
||||
tests := []struct {
|
||||
baseURL string
|
||||
url string
|
||||
expected string
|
||||
}{
|
||||
{"http://example.com/sub/", "/foo", "/sub/foo"},
|
||||
{"http://example.com/sub/", "/foo/index.html", "/sub/foo/index.html"},
|
||||
{"http://example.com/sub1/sub2", "/foo", "/sub1/sub2/foo"},
|
||||
{"http://example.com", "/foo", "/foo"},
|
||||
// cannot guess that the context root is already added int the example below
|
||||
{"http://example.com/sub/", "/sub/foo", "/sub/sub/foo"},
|
||||
{"http://example.com/тря", "/трям/", "/тря/трям/"},
|
||||
{"http://example.com", "/", "/"},
|
||||
{"http://example.com/bar", "//", "/bar/"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
output := AddContextRoot(test.baseURL, test.url)
|
||||
if output != test.expected {
|
||||
t.Errorf("Expected %#v, got %#v\n", test.expected, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPretty(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
c.Assert("/section/name/index.html", qt.Equals, PrettifyURLPath("/section/name.html"))
|
||||
c.Assert("/section/sub/name/index.html", qt.Equals, PrettifyURLPath("/section/sub/name.html"))
|
||||
c.Assert("/section/name/index.html", qt.Equals, PrettifyURLPath("/section/name/"))
|
||||
c.Assert("/section/name/index.html", qt.Equals, PrettifyURLPath("/section/name/index.html"))
|
||||
c.Assert("/index.html", qt.Equals, PrettifyURLPath("/index.html"))
|
||||
c.Assert("/name/index.xml", qt.Equals, PrettifyURLPath("/name.xml"))
|
||||
c.Assert("/", qt.Equals, PrettifyURLPath("/"))
|
||||
c.Assert("/", qt.Equals, PrettifyURLPath(""))
|
||||
c.Assert("/section/name", qt.Equals, PrettifyURL("/section/name.html"))
|
||||
c.Assert("/section/sub/name", qt.Equals, PrettifyURL("/section/sub/name.html"))
|
||||
c.Assert("/section/name", qt.Equals, PrettifyURL("/section/name/"))
|
||||
c.Assert("/section/name", qt.Equals, PrettifyURL("/section/name/index.html"))
|
||||
c.Assert("/", qt.Equals, PrettifyURL("/index.html"))
|
||||
c.Assert("/name/index.xml", qt.Equals, PrettifyURL("/name.xml"))
|
||||
c.Assert("/", qt.Equals, PrettifyURL("/"))
|
||||
c.Assert("/", qt.Equals, PrettifyURL(""))
|
||||
}
|
||||
|
||||
func TestUgly(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
c.Assert("/section/name.html", qt.Equals, Uglify("/section/name.html"))
|
||||
c.Assert("/section/sub/name.html", qt.Equals, Uglify("/section/sub/name.html"))
|
||||
c.Assert("/section/name.html", qt.Equals, Uglify("/section/name/"))
|
||||
c.Assert("/section/name.html", qt.Equals, Uglify("/section/name/index.html"))
|
||||
c.Assert("/index.html", qt.Equals, Uglify("/index.html"))
|
||||
c.Assert("/name.xml", qt.Equals, Uglify("/name.xml"))
|
||||
c.Assert("/", qt.Equals, Uglify("/"))
|
||||
c.Assert("/", qt.Equals, Uglify(""))
|
||||
}
|
||||
@@ -50,12 +50,11 @@ func (pos Position) IsValid() bool {
|
||||
var positionStringFormatfunc func(p Position) string
|
||||
|
||||
func createPositionStringFormatter(formatStr string) func(p Position) string {
|
||||
|
||||
if formatStr == "" {
|
||||
formatStr = "\":file::line::col\""
|
||||
}
|
||||
|
||||
var identifiers = []string{":file", ":line", ":col"}
|
||||
identifiers := []string{":file", ":line", ":col"}
|
||||
var identifiersFound []string
|
||||
|
||||
for i := range formatStr {
|
||||
|
||||
@@ -29,5 +29,4 @@ func TestPositionStringFormatter(t *testing.T) {
|
||||
c.Assert(createPositionStringFormatter("好::col")(pos), qt.Equals, "好:13")
|
||||
c.Assert(createPositionStringFormatter("")(pos), qt.Equals, "\"/my/file.txt:12:13\"")
|
||||
c.Assert(pos.String(), qt.Equals, "\"/my/file.txt:12:13\"")
|
||||
|
||||
}
|
||||
|
||||
@@ -25,5 +25,4 @@ func TestRemoveAccents(t *testing.T) {
|
||||
c.Assert(string(RemoveAccents([]byte("Resumé"))), qt.Equals, "Resume")
|
||||
c.Assert(string(RemoveAccents([]byte("Hugo Rocks!"))), qt.Equals, "Hugo Rocks!")
|
||||
c.Assert(string(RemoveAccentsString("Resumé")), qt.Equals, "Resume")
|
||||
|
||||
}
|
||||
|
||||
+70
-8
@@ -14,21 +14,73 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
// ToStringSlicePreserveString converts v to a string slice.
|
||||
// If v is a string, it will be wrapped in a string slice.
|
||||
// ToDuration converts v to time.Duration.
|
||||
// See ToDurationE if you need to handle errors.
|
||||
func ToDuration(v interface{}) time.Duration {
|
||||
d, _ := ToDurationE(v)
|
||||
return d
|
||||
}
|
||||
|
||||
// ToDurationE converts v to time.Duration.
|
||||
func ToDurationE(v interface{}) (time.Duration, error) {
|
||||
if n := cast.ToInt(v); n > 0 {
|
||||
return time.Duration(n) * time.Millisecond, nil
|
||||
}
|
||||
d, err := time.ParseDuration(cast.ToString(v))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("cannot convert %v to time.Duration", v)
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// ToStringSlicePreserveString is the same as ToStringSlicePreserveStringE,
|
||||
// but it never fails.
|
||||
func ToStringSlicePreserveString(v interface{}) []string {
|
||||
vv, _ := ToStringSlicePreserveStringE(v)
|
||||
return vv
|
||||
}
|
||||
|
||||
// ToStringSlicePreserveStringE converts v to a string slice.
|
||||
// If v is a string, it will be wrapped in a string slice.
|
||||
func ToStringSlicePreserveStringE(v interface{}) ([]string, error) {
|
||||
if v == nil {
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
if sds, ok := v.(string); ok {
|
||||
return []string{sds}
|
||||
return []string{sds}, nil
|
||||
}
|
||||
return cast.ToStringSlice(v)
|
||||
result, err := cast.ToStringSliceE(v)
|
||||
if err == nil {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Probably []int or similar. Fall back to reflect.
|
||||
vv := reflect.ValueOf(v)
|
||||
|
||||
switch vv.Kind() {
|
||||
case reflect.Slice, reflect.Array:
|
||||
result = make([]string, vv.Len())
|
||||
for i := 0; i < vv.Len(); i++ {
|
||||
s, err := cast.ToStringE(vv.Index(i).Interface())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[i] = s
|
||||
}
|
||||
return result, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("failed to convert %T to a string slice", v)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// TypeToString converts v to a string if it's a valid string type.
|
||||
@@ -59,10 +111,20 @@ func TypeToString(v interface{}) (string, bool) {
|
||||
|
||||
// ToString converts v to a string.
|
||||
func ToString(v interface{}) string {
|
||||
s, _ := ToStringE(v)
|
||||
return s
|
||||
}
|
||||
|
||||
// ToStringE converts v to a string.
|
||||
func ToStringE(v interface{}) (string, error) {
|
||||
if s, ok := TypeToString(v); ok {
|
||||
return s
|
||||
return s, nil
|
||||
}
|
||||
|
||||
return cast.ToString(v)
|
||||
|
||||
switch s := v.(type) {
|
||||
case json.RawMessage:
|
||||
return string(s), nil
|
||||
default:
|
||||
return cast.ToStringE(v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,9 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
@@ -23,7 +25,25 @@ func TestToStringSlicePreserveString(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
c.Assert(ToStringSlicePreserveString("Hugo"), qt.DeepEquals, []string{"Hugo"})
|
||||
c.Assert(ToStringSlicePreserveString(qt.Commentf("Hugo")), qt.DeepEquals, []string{"Hugo"})
|
||||
c.Assert(ToStringSlicePreserveString([]interface{}{"A", "B"}), qt.DeepEquals, []string{"A", "B"})
|
||||
c.Assert(ToStringSlicePreserveString([]int{1, 3}), qt.DeepEquals, []string{"1", "3"})
|
||||
c.Assert(ToStringSlicePreserveString(nil), qt.IsNil)
|
||||
}
|
||||
|
||||
func TestToString(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
c.Assert(ToString([]byte("Hugo")), qt.Equals, "Hugo")
|
||||
c.Assert(ToString(json.RawMessage("Hugo")), qt.Equals, "Hugo")
|
||||
}
|
||||
|
||||
func TestToDuration(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
c.Assert(ToDuration("200ms"), qt.Equals, 200*time.Millisecond)
|
||||
c.Assert(ToDuration("200"), qt.Equals, 200*time.Millisecond)
|
||||
c.Assert(ToDuration("4m"), qt.Equals, 4*time.Minute)
|
||||
c.Assert(ToDuration("asdfadf"), qt.Equals, time.Duration(0))
|
||||
|
||||
}
|
||||
|
||||
@@ -27,6 +27,12 @@ type RLocker interface {
|
||||
RUnlock()
|
||||
}
|
||||
|
||||
// KeyValue is a interface{} tuple.
|
||||
type KeyValue struct {
|
||||
Key interface{}
|
||||
Value interface{}
|
||||
}
|
||||
|
||||
// KeyValueStr is a string tuple.
|
||||
type KeyValueStr struct {
|
||||
Key string
|
||||
|
||||
@@ -61,5 +61,4 @@ func TestLexicographicSort(t *testing.T) {
|
||||
})
|
||||
|
||||
c.Assert(s, qt.DeepEquals, []string{"A", "b", "Ba", "ba", "ba", "Bz"})
|
||||
|
||||
}
|
||||
|
||||
+74
-12
@@ -18,6 +18,8 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
|
||||
"github.com/gobwas/glob"
|
||||
@@ -32,13 +34,17 @@ var DefaultBuild = Build{
|
||||
WriteStats: false,
|
||||
}
|
||||
|
||||
// Build holds some build related condfiguration.
|
||||
// Build holds some build related configuration.
|
||||
type Build struct {
|
||||
UseResourceCacheWhen string // never, fallback, always. Default is fallback
|
||||
|
||||
// When enabled, will collect and write a hugo_stats.json with some build
|
||||
// related aggregated data (e.g. CSS class names).
|
||||
WriteStats bool
|
||||
|
||||
// Can be used to toggle off writing of the intellinsense /assets/jsconfig.js
|
||||
// file.
|
||||
NoJSConfigInAssets bool
|
||||
}
|
||||
|
||||
func (b Build) UseResourceCache(err error) bool {
|
||||
@@ -82,7 +88,6 @@ type Sitemap struct {
|
||||
}
|
||||
|
||||
func DecodeSitemap(prototype Sitemap, input map[string]interface{}) Sitemap {
|
||||
|
||||
for key, value := range input {
|
||||
switch key {
|
||||
case "changefreq":
|
||||
@@ -101,26 +106,35 @@ func DecodeSitemap(prototype Sitemap, input map[string]interface{}) Sitemap {
|
||||
|
||||
// Config for the dev server.
|
||||
type Server struct {
|
||||
Headers []Headers
|
||||
Headers []Headers
|
||||
Redirects []Redirect
|
||||
|
||||
compiledInit sync.Once
|
||||
compiled []glob.Glob
|
||||
compiledInit sync.Once
|
||||
compiledHeaders []glob.Glob
|
||||
compiledRedirects []glob.Glob
|
||||
}
|
||||
|
||||
func (s *Server) Match(pattern string) []types.KeyValueStr {
|
||||
func (s *Server) init() {
|
||||
s.compiledInit.Do(func() {
|
||||
for _, h := range s.Headers {
|
||||
s.compiled = append(s.compiled, glob.MustCompile(h.For))
|
||||
s.compiledHeaders = append(s.compiledHeaders, glob.MustCompile(h.For))
|
||||
}
|
||||
for _, r := range s.Redirects {
|
||||
s.compiledRedirects = append(s.compiledRedirects, glob.MustCompile(r.From))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if s.compiled == nil {
|
||||
func (s *Server) MatchHeaders(pattern string) []types.KeyValueStr {
|
||||
s.init()
|
||||
|
||||
if s.compiledHeaders == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var matches []types.KeyValueStr
|
||||
|
||||
for i, g := range s.compiled {
|
||||
for i, g := range s.compiledHeaders {
|
||||
if g.Match(pattern) {
|
||||
h := s.Headers[i]
|
||||
for k, v := range h.Values {
|
||||
@@ -134,7 +148,31 @@ func (s *Server) Match(pattern string) []types.KeyValueStr {
|
||||
})
|
||||
|
||||
return matches
|
||||
}
|
||||
|
||||
func (s *Server) MatchRedirect(pattern string) Redirect {
|
||||
s.init()
|
||||
|
||||
if s.compiledRedirects == nil {
|
||||
return Redirect{}
|
||||
}
|
||||
|
||||
pattern = strings.TrimSuffix(pattern, "index.html")
|
||||
|
||||
for i, g := range s.compiledRedirects {
|
||||
redir := s.Redirects[i]
|
||||
|
||||
// No redirect to self.
|
||||
if redir.To == pattern {
|
||||
return Redirect{}
|
||||
}
|
||||
|
||||
if g.Match(pattern) {
|
||||
return redir
|
||||
}
|
||||
}
|
||||
|
||||
return Redirect{}
|
||||
}
|
||||
|
||||
type Headers struct {
|
||||
@@ -142,13 +180,37 @@ type Headers struct {
|
||||
Values map[string]interface{}
|
||||
}
|
||||
|
||||
func DecodeServer(cfg Provider) *Server {
|
||||
type Redirect struct {
|
||||
From string
|
||||
To string
|
||||
Status int
|
||||
Force bool
|
||||
}
|
||||
|
||||
func (r Redirect) IsZero() bool {
|
||||
return r.From == ""
|
||||
}
|
||||
|
||||
func DecodeServer(cfg Provider) (*Server, error) {
|
||||
m := cfg.GetStringMap("server")
|
||||
s := &Server{}
|
||||
if m == nil {
|
||||
return s
|
||||
return s, nil
|
||||
}
|
||||
|
||||
_ = mapstructure.WeakDecode(m, s)
|
||||
return s
|
||||
|
||||
for i, redir := range s.Redirects {
|
||||
// Get it in line with the Hugo server.
|
||||
redir.To = strings.TrimSuffix(redir.To, "index.html")
|
||||
if !strings.HasPrefix(redir.To, "https") && !strings.HasSuffix(redir.To, "/") {
|
||||
// There are some tricky infinite loop situations when dealing
|
||||
// when the target does not have a trailing slash.
|
||||
// This can certainly be handled better, but not time for that now.
|
||||
return nil, errors.Errorf("unsupported redirect to value %q in server config; currently this must be either a remote destination or a local folder, e.g. \"/blog/\" or \"/blog/index.html\"", redir.To)
|
||||
}
|
||||
s.Redirects[i] = redir
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
@@ -21,14 +21,12 @@ import (
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func TestBuild(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
v := viper.New()
|
||||
v := New()
|
||||
v.Set("build", map[string]interface{}{
|
||||
"useResourceCacheWhen": "always",
|
||||
})
|
||||
@@ -57,7 +55,6 @@ func TestBuild(t *testing.T) {
|
||||
c.Assert(b.UseResourceCache(herrors.ErrFeatureNotAvailable), qt.Equals, false)
|
||||
c.Assert(b.UseResourceCache(errors.New("err")), qt.Equals, false)
|
||||
c.Assert(b.UseResourceCache(nil), qt.Equals, false)
|
||||
|
||||
}
|
||||
|
||||
func TestServer(t *testing.T) {
|
||||
@@ -70,15 +67,74 @@ for = "/*.jpg"
|
||||
X-Frame-Options = "DENY"
|
||||
X-XSS-Protection = "1; mode=block"
|
||||
X-Content-Type-Options = "nosniff"
|
||||
|
||||
[[server.redirects]]
|
||||
from = "/foo/**"
|
||||
to = "/foo/index.html"
|
||||
status = 200
|
||||
|
||||
[[server.redirects]]
|
||||
from = "/google/**"
|
||||
to = "https://google.com/"
|
||||
status = 301
|
||||
|
||||
[[server.redirects]]
|
||||
from = "/**"
|
||||
to = "/default/index.html"
|
||||
status = 301
|
||||
|
||||
|
||||
|
||||
`, "toml")
|
||||
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
s := DecodeServer(cfg)
|
||||
s, err := DecodeServer(cfg)
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
c.Assert(s.Match("/foo.jpg"), qt.DeepEquals, []types.KeyValueStr{
|
||||
c.Assert(s.MatchHeaders("/foo.jpg"), qt.DeepEquals, []types.KeyValueStr{
|
||||
{Key: "X-Content-Type-Options", Value: "nosniff"},
|
||||
{Key: "X-Frame-Options", Value: "DENY"},
|
||||
{Key: "X-XSS-Protection", Value: "1; mode=block"}})
|
||||
{Key: "X-XSS-Protection", Value: "1; mode=block"},
|
||||
})
|
||||
|
||||
c.Assert(s.MatchRedirect("/foo/bar/baz"), qt.DeepEquals, Redirect{
|
||||
From: "/foo/**",
|
||||
To: "/foo/",
|
||||
Status: 200,
|
||||
})
|
||||
|
||||
c.Assert(s.MatchRedirect("/someother"), qt.DeepEquals, Redirect{
|
||||
From: "/**",
|
||||
To: "/default/",
|
||||
Status: 301,
|
||||
})
|
||||
|
||||
c.Assert(s.MatchRedirect("/google/foo"), qt.DeepEquals, Redirect{
|
||||
From: "/google/**",
|
||||
To: "https://google.com/",
|
||||
Status: 301,
|
||||
})
|
||||
|
||||
// No redirect loop, please.
|
||||
c.Assert(s.MatchRedirect("/default/index.html"), qt.DeepEquals, Redirect{})
|
||||
c.Assert(s.MatchRedirect("/default/"), qt.DeepEquals, Redirect{})
|
||||
|
||||
for _, errorCase := range []string{
|
||||
`[[server.redirects]]
|
||||
from = "/**"
|
||||
to = "/file"
|
||||
status = 301`,
|
||||
`[[server.redirects]]
|
||||
from = "/**"
|
||||
to = "/foo/file.html"
|
||||
status = 301`,
|
||||
} {
|
||||
|
||||
cfg, err := FromConfigString(errorCase, "toml")
|
||||
c.Assert(err, qt.IsNil)
|
||||
_, err = DecodeServer(cfg)
|
||||
c.Assert(err, qt.Not(qt.IsNil))
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
// Copyright 2021 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
)
|
||||
|
||||
// NewCompositeConfig creates a new composite Provider with a read-only base
|
||||
// and a writeable layer.
|
||||
func NewCompositeConfig(base, layer Provider) Provider {
|
||||
return &compositeConfig{
|
||||
base: base,
|
||||
layer: layer,
|
||||
}
|
||||
}
|
||||
|
||||
// compositeConfig contains a read only config base with
|
||||
// a possibly writeable config layer on top.
|
||||
type compositeConfig struct {
|
||||
base Provider
|
||||
layer Provider
|
||||
}
|
||||
|
||||
func (c *compositeConfig) GetBool(key string) bool {
|
||||
if c.layer.IsSet(key) {
|
||||
return c.layer.GetBool(key)
|
||||
}
|
||||
return c.base.GetBool(key)
|
||||
}
|
||||
|
||||
func (c *compositeConfig) GetInt(key string) int {
|
||||
if c.layer.IsSet(key) {
|
||||
return c.layer.GetInt(key)
|
||||
}
|
||||
return c.base.GetInt(key)
|
||||
}
|
||||
|
||||
func (c *compositeConfig) Merge(key string, value interface{}) {
|
||||
c.layer.Merge(key, value)
|
||||
}
|
||||
|
||||
func (c *compositeConfig) GetParams(key string) maps.Params {
|
||||
if c.layer.IsSet(key) {
|
||||
return c.layer.GetParams(key)
|
||||
}
|
||||
return c.base.GetParams(key)
|
||||
}
|
||||
|
||||
func (c *compositeConfig) GetStringMap(key string) map[string]interface{} {
|
||||
if c.layer.IsSet(key) {
|
||||
return c.layer.GetStringMap(key)
|
||||
}
|
||||
return c.base.GetStringMap(key)
|
||||
}
|
||||
|
||||
func (c *compositeConfig) GetStringMapString(key string) map[string]string {
|
||||
if c.layer.IsSet(key) {
|
||||
return c.layer.GetStringMapString(key)
|
||||
}
|
||||
return c.base.GetStringMapString(key)
|
||||
}
|
||||
|
||||
func (c *compositeConfig) GetStringSlice(key string) []string {
|
||||
if c.layer.IsSet(key) {
|
||||
return c.layer.GetStringSlice(key)
|
||||
}
|
||||
return c.base.GetStringSlice(key)
|
||||
}
|
||||
|
||||
func (c *compositeConfig) Get(key string) interface{} {
|
||||
if c.layer.IsSet(key) {
|
||||
return c.layer.Get(key)
|
||||
}
|
||||
return c.base.Get(key)
|
||||
}
|
||||
|
||||
func (c *compositeConfig) IsSet(key string) bool {
|
||||
if c.layer.IsSet(key) {
|
||||
return true
|
||||
}
|
||||
return c.base.IsSet(key)
|
||||
}
|
||||
|
||||
func (c *compositeConfig) GetString(key string) string {
|
||||
if c.layer.IsSet(key) {
|
||||
return c.layer.GetString(key)
|
||||
}
|
||||
return c.base.GetString(key)
|
||||
}
|
||||
|
||||
func (c *compositeConfig) Set(key string, value interface{}) {
|
||||
c.layer.Set(key, value)
|
||||
}
|
||||
|
||||
func (c *compositeConfig) SetDefaults(params maps.Params) {
|
||||
c.layer.SetDefaults(params)
|
||||
}
|
||||
|
||||
func (c *compositeConfig) WalkParams(walkFn func(params ...KeyParams) bool) {
|
||||
panic("not supported")
|
||||
}
|
||||
|
||||
func (c *compositeConfig) SetDefaultMergeStrategy() {
|
||||
panic("not supported")
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright 2021 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestCompositeConfig(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
c.Run("Set and get", func(c *qt.C) {
|
||||
base, layer := New(), New()
|
||||
cfg := NewCompositeConfig(base, layer)
|
||||
|
||||
layer.Set("a1", "av")
|
||||
base.Set("b1", "bv")
|
||||
cfg.Set("c1", "cv")
|
||||
|
||||
c.Assert(cfg.Get("a1"), qt.Equals, "av")
|
||||
c.Assert(cfg.Get("b1"), qt.Equals, "bv")
|
||||
c.Assert(cfg.Get("c1"), qt.Equals, "cv")
|
||||
c.Assert(cfg.IsSet("c1"), qt.IsTrue)
|
||||
c.Assert(layer.IsSet("c1"), qt.IsTrue)
|
||||
c.Assert(base.IsSet("c1"), qt.IsFalse)
|
||||
})
|
||||
}
|
||||
+103
-22
@@ -14,13 +14,17 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/gohugoio/hugo/common/paths"
|
||||
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/parser/metadecoders"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -43,15 +47,11 @@ func IsValidConfigFilename(filename string) bool {
|
||||
|
||||
// FromConfigString creates a config from the given YAML, JSON or TOML config. This is useful in tests.
|
||||
func FromConfigString(config, configType string) (Provider, error) {
|
||||
v := newViper()
|
||||
m, err := readConfig(metadecoders.FormatFromString(configType), []byte(config))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v.MergeConfigMap(m)
|
||||
|
||||
return v, nil
|
||||
return NewFrom(m), nil
|
||||
}
|
||||
|
||||
// FromFile loads the configuration from the given filename.
|
||||
@@ -60,15 +60,7 @@ func FromFile(fs afero.Fs, filename string) (Provider, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v := newViper()
|
||||
|
||||
err = v.MergeConfigMap(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return v, nil
|
||||
return NewFrom(m), nil
|
||||
}
|
||||
|
||||
// FromFileToMap is the same as FromFile, but it returns the config values
|
||||
@@ -86,7 +78,6 @@ func readConfig(format metadecoders.Format, data []byte) (map[string]interface{}
|
||||
RenameKeys(m)
|
||||
|
||||
return m, nil
|
||||
|
||||
}
|
||||
|
||||
func loadConfigFromFile(fs afero.Fs, filename string) (map[string]interface{}, error) {
|
||||
@@ -98,6 +89,102 @@ func loadConfigFromFile(fs afero.Fs, filename string) (map[string]interface{}, e
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func LoadConfigFromDir(sourceFs afero.Fs, configDir, environment string) (Provider, []string, error) {
|
||||
defaultConfigDir := filepath.Join(configDir, "_default")
|
||||
environmentConfigDir := filepath.Join(configDir, environment)
|
||||
cfg := New()
|
||||
|
||||
var configDirs []string
|
||||
// Merge from least to most specific.
|
||||
for _, dir := range []string{defaultConfigDir, environmentConfigDir} {
|
||||
if _, err := sourceFs.Stat(dir); err == nil {
|
||||
configDirs = append(configDirs, dir)
|
||||
}
|
||||
}
|
||||
|
||||
if len(configDirs) == 0 {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
// Keep track of these so we can watch them for changes.
|
||||
var dirnames []string
|
||||
|
||||
for _, configDir := range configDirs {
|
||||
err := afero.Walk(sourceFs, configDir, func(path string, fi os.FileInfo, err error) error {
|
||||
if fi == nil || err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if fi.IsDir() {
|
||||
dirnames = append(dirnames, path)
|
||||
return nil
|
||||
}
|
||||
|
||||
if !IsValidConfigFilename(path) {
|
||||
return nil
|
||||
}
|
||||
|
||||
name := paths.Filename(filepath.Base(path))
|
||||
|
||||
item, err := metadecoders.Default.UnmarshalFileToMap(sourceFs, path)
|
||||
if err != nil {
|
||||
// This will be used in error reporting, use the most specific value.
|
||||
dirnames = []string{path}
|
||||
return errors.Wrapf(err, "failed to unmarshl config for path %q", path)
|
||||
}
|
||||
|
||||
var keyPath []string
|
||||
|
||||
if name != "config" {
|
||||
// Can be params.jp, menus.en etc.
|
||||
name, lang := paths.FileAndExtNoDelimiter(name)
|
||||
|
||||
keyPath = []string{name}
|
||||
|
||||
if lang != "" {
|
||||
keyPath = []string{"languages", lang}
|
||||
switch name {
|
||||
case "menu", "menus":
|
||||
keyPath = append(keyPath, "menus")
|
||||
case "params":
|
||||
keyPath = append(keyPath, "params")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
root := item
|
||||
if len(keyPath) > 0 {
|
||||
root = make(map[string]interface{})
|
||||
m := root
|
||||
for i, key := range keyPath {
|
||||
if i >= len(keyPath)-1 {
|
||||
m[key] = item
|
||||
} else {
|
||||
nm := make(map[string]interface{})
|
||||
m[key] = nm
|
||||
m = nm
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate menu => menus etc.
|
||||
RenameKeys(root)
|
||||
|
||||
// Set will overwrite keys with the same name, recursively.
|
||||
cfg.Set("", root)
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, dirnames, err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return cfg, dirnames, nil
|
||||
|
||||
}
|
||||
|
||||
var keyAliases maps.KeyRenamer
|
||||
|
||||
func init() {
|
||||
@@ -117,9 +204,3 @@ func init() {
|
||||
func RenameKeys(m map[string]interface{}) {
|
||||
keyAliases.Rename(m)
|
||||
}
|
||||
|
||||
func newViper() *viper.Viper {
|
||||
v := viper.New()
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
)
|
||||
|
||||
@@ -22,11 +23,16 @@ type Provider interface {
|
||||
GetString(key string) string
|
||||
GetInt(key string) int
|
||||
GetBool(key string) bool
|
||||
GetParams(key string) maps.Params
|
||||
GetStringMap(key string) map[string]interface{}
|
||||
GetStringMapString(key string) map[string]string
|
||||
GetStringSlice(key string) []string
|
||||
Get(key string) interface{}
|
||||
Set(key string, value interface{})
|
||||
Merge(key string, value interface{})
|
||||
SetDefaults(params maps.Params)
|
||||
SetDefaultMergeStrategy()
|
||||
WalkParams(walkFn func(params ...KeyParams) bool)
|
||||
IsSet(key string) bool
|
||||
}
|
||||
|
||||
|
||||
@@ -17,12 +17,11 @@ import (
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func TestGetStringSlicePreserveString(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
cfg := viper.New()
|
||||
cfg := New()
|
||||
|
||||
s := "This is a string"
|
||||
sSlice := []string{"This", "is", "a", "slice"}
|
||||
|
||||
@@ -0,0 +1,439 @@
|
||||
// Copyright 2021 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
// ConfigRootKeysSet contains all of the config map root keys.
|
||||
ConfigRootKeysSet = map[string]bool{
|
||||
"build": true,
|
||||
"caches": true,
|
||||
"cascade": true,
|
||||
"frontmatter": true,
|
||||
"languages": true,
|
||||
"imaging": true,
|
||||
"markup": true,
|
||||
"mediatypes": true,
|
||||
"menus": true,
|
||||
"minify": true,
|
||||
"module": true,
|
||||
"outputformats": true,
|
||||
"params": true,
|
||||
"permalinks": true,
|
||||
"related": true,
|
||||
"sitemap": true,
|
||||
"taxonomies": true,
|
||||
}
|
||||
|
||||
// ConfigRootKeys is a sorted version of ConfigRootKeysSet.
|
||||
ConfigRootKeys []string
|
||||
)
|
||||
|
||||
func init() {
|
||||
for k := range ConfigRootKeysSet {
|
||||
ConfigRootKeys = append(ConfigRootKeys, k)
|
||||
}
|
||||
sort.Strings(ConfigRootKeys)
|
||||
}
|
||||
|
||||
// New creates a Provider backed by an empty maps.Params.
|
||||
func New() Provider {
|
||||
return &defaultConfigProvider{
|
||||
root: make(maps.Params),
|
||||
}
|
||||
}
|
||||
|
||||
// NewFrom creates a Provider backed by params.
|
||||
func NewFrom(params maps.Params) Provider {
|
||||
maps.PrepareParams(params)
|
||||
return &defaultConfigProvider{
|
||||
root: params,
|
||||
}
|
||||
}
|
||||
|
||||
// defaultConfigProvider is a Provider backed by a map where all keys are lower case.
|
||||
// All methods are thread safe.
|
||||
type defaultConfigProvider struct {
|
||||
mu sync.RWMutex
|
||||
root maps.Params
|
||||
|
||||
keyCache sync.Map
|
||||
}
|
||||
|
||||
func (c *defaultConfigProvider) Get(k string) interface{} {
|
||||
if k == "" {
|
||||
return c.root
|
||||
}
|
||||
c.mu.RLock()
|
||||
key, m := c.getNestedKeyAndMap(strings.ToLower(k), false)
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
v := m[key]
|
||||
c.mu.RUnlock()
|
||||
return v
|
||||
}
|
||||
|
||||
func (c *defaultConfigProvider) GetBool(k string) bool {
|
||||
v := c.Get(k)
|
||||
return cast.ToBool(v)
|
||||
}
|
||||
|
||||
func (c *defaultConfigProvider) GetInt(k string) int {
|
||||
v := c.Get(k)
|
||||
return cast.ToInt(v)
|
||||
}
|
||||
|
||||
func (c *defaultConfigProvider) IsSet(k string) bool {
|
||||
var found bool
|
||||
c.mu.RLock()
|
||||
key, m := c.getNestedKeyAndMap(strings.ToLower(k), false)
|
||||
if m != nil {
|
||||
_, found = m[key]
|
||||
}
|
||||
c.mu.RUnlock()
|
||||
return found
|
||||
}
|
||||
|
||||
func (c *defaultConfigProvider) GetString(k string) string {
|
||||
v := c.Get(k)
|
||||
return cast.ToString(v)
|
||||
}
|
||||
|
||||
func (c *defaultConfigProvider) GetParams(k string) maps.Params {
|
||||
v := c.Get(k)
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
return v.(maps.Params)
|
||||
}
|
||||
|
||||
func (c *defaultConfigProvider) GetStringMap(k string) map[string]interface{} {
|
||||
v := c.Get(k)
|
||||
return maps.ToStringMap(v)
|
||||
}
|
||||
|
||||
func (c *defaultConfigProvider) GetStringMapString(k string) map[string]string {
|
||||
v := c.Get(k)
|
||||
return maps.ToStringMapString(v)
|
||||
}
|
||||
|
||||
func (c *defaultConfigProvider) GetStringSlice(k string) []string {
|
||||
v := c.Get(k)
|
||||
return cast.ToStringSlice(v)
|
||||
}
|
||||
|
||||
func (c *defaultConfigProvider) Set(k string, v interface{}) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
k = strings.ToLower(k)
|
||||
|
||||
if k == "" {
|
||||
if p, ok := maps.ToParamsAndPrepare(v); ok {
|
||||
// Set the values directly in root.
|
||||
c.root.Set(p)
|
||||
} else {
|
||||
c.root[k] = v
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
switch vv := v.(type) {
|
||||
case map[string]interface{}, map[interface{}]interface{}, map[string]string:
|
||||
p := maps.MustToParamsAndPrepare(vv)
|
||||
v = p
|
||||
}
|
||||
|
||||
key, m := c.getNestedKeyAndMap(k, true)
|
||||
if m == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if existing, found := m[key]; found {
|
||||
if p1, ok := existing.(maps.Params); ok {
|
||||
if p2, ok := v.(maps.Params); ok {
|
||||
p1.Set(p2)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m[key] = v
|
||||
}
|
||||
|
||||
// SetDefaults will set values from params if not already set.
|
||||
func (c *defaultConfigProvider) SetDefaults(params maps.Params) {
|
||||
maps.PrepareParams(params)
|
||||
for k, v := range params {
|
||||
if _, found := c.root[k]; !found {
|
||||
c.root[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *defaultConfigProvider) Merge(k string, v interface{}) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
k = strings.ToLower(k)
|
||||
|
||||
const (
|
||||
languagesKey = "languages"
|
||||
paramsKey = "params"
|
||||
menusKey = "menus"
|
||||
)
|
||||
|
||||
if k == "" {
|
||||
rs, f := c.root.GetMergeStrategy()
|
||||
if f && rs == maps.ParamsMergeStrategyNone {
|
||||
// The user has set a "no merge" strategy on this,
|
||||
// nothing more to do.
|
||||
return
|
||||
}
|
||||
|
||||
if p, ok := maps.ToParamsAndPrepare(v); ok {
|
||||
// As there may be keys in p not in root, we need to handle
|
||||
// those as a special case.
|
||||
var keysToDelete []string
|
||||
for kk, vv := range p {
|
||||
if pp, ok := vv.(maps.Params); ok {
|
||||
if pppi, ok := c.root[kk]; ok {
|
||||
ppp := pppi.(maps.Params)
|
||||
if kk == languagesKey {
|
||||
// Languages is currently a special case.
|
||||
// We may have languages with menus or params in the
|
||||
// right map that is not present in the left map.
|
||||
// With the default merge strategy those items will not
|
||||
// be passed over.
|
||||
var hasParams, hasMenus bool
|
||||
for _, rv := range pp {
|
||||
if lkp, ok := rv.(maps.Params); ok {
|
||||
_, hasMenus = lkp[menusKey]
|
||||
_, hasParams = lkp[paramsKey]
|
||||
}
|
||||
}
|
||||
|
||||
if hasMenus || hasParams {
|
||||
for _, lv := range ppp {
|
||||
if lkp, ok := lv.(maps.Params); ok {
|
||||
if hasMenus {
|
||||
if _, ok := lkp[menusKey]; !ok {
|
||||
p := maps.Params{}
|
||||
p.SetDefaultMergeStrategy(maps.ParamsMergeStrategyShallow)
|
||||
lkp[menusKey] = p
|
||||
}
|
||||
}
|
||||
if hasParams {
|
||||
if _, ok := lkp[paramsKey]; !ok {
|
||||
p := maps.Params{}
|
||||
p.SetDefaultMergeStrategy(maps.ParamsMergeStrategyShallow)
|
||||
lkp[paramsKey] = p
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ppp.Merge(pp)
|
||||
} else {
|
||||
// We need to use the default merge strategy for
|
||||
// this key.
|
||||
np := make(maps.Params)
|
||||
strategy := c.determineMergeStrategy(KeyParams{Key: "", Params: c.root}, KeyParams{Key: kk, Params: np})
|
||||
np.SetDefaultMergeStrategy(strategy)
|
||||
np.Merge(pp)
|
||||
c.root[kk] = np
|
||||
if np.IsZero() {
|
||||
// Just keep it until merge is done.
|
||||
keysToDelete = append(keysToDelete, kk)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Merge the rest.
|
||||
c.root.Merge(p)
|
||||
for _, k := range keysToDelete {
|
||||
delete(c.root, k)
|
||||
}
|
||||
} else {
|
||||
panic(fmt.Sprintf("unsupported type %T received in Merge", v))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
switch vv := v.(type) {
|
||||
case map[string]interface{}, map[interface{}]interface{}, map[string]string:
|
||||
p := maps.MustToParamsAndPrepare(vv)
|
||||
v = p
|
||||
}
|
||||
|
||||
key, m := c.getNestedKeyAndMap(k, true)
|
||||
if m == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if existing, found := m[key]; found {
|
||||
if p1, ok := existing.(maps.Params); ok {
|
||||
if p2, ok := v.(maps.Params); ok {
|
||||
p1.Merge(p2)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
m[key] = v
|
||||
}
|
||||
}
|
||||
|
||||
func (c *defaultConfigProvider) WalkParams(walkFn func(params ...KeyParams) bool) {
|
||||
var walk func(params ...KeyParams)
|
||||
walk = func(params ...KeyParams) {
|
||||
if walkFn(params...) {
|
||||
return
|
||||
}
|
||||
p1 := params[len(params)-1]
|
||||
i := len(params)
|
||||
for k, v := range p1.Params {
|
||||
if p2, ok := v.(maps.Params); ok {
|
||||
paramsplus1 := make([]KeyParams, i+1)
|
||||
copy(paramsplus1, params)
|
||||
paramsplus1[i] = KeyParams{Key: k, Params: p2}
|
||||
walk(paramsplus1...)
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(KeyParams{Key: "", Params: c.root})
|
||||
}
|
||||
|
||||
func (c *defaultConfigProvider) determineMergeStrategy(params ...KeyParams) maps.ParamsMergeStrategy {
|
||||
if len(params) == 0 {
|
||||
return maps.ParamsMergeStrategyNone
|
||||
}
|
||||
|
||||
var (
|
||||
strategy maps.ParamsMergeStrategy
|
||||
prevIsRoot bool
|
||||
curr = params[len(params)-1]
|
||||
)
|
||||
|
||||
if len(params) > 1 {
|
||||
prev := params[len(params)-2]
|
||||
prevIsRoot = prev.Key == ""
|
||||
|
||||
// Inherit from parent (but not from the root unless it's set by user).
|
||||
s, found := prev.Params.GetMergeStrategy()
|
||||
if !prevIsRoot && !found {
|
||||
panic("invalid state, merge strategy not set on parent")
|
||||
}
|
||||
if found || !prevIsRoot {
|
||||
strategy = s
|
||||
}
|
||||
}
|
||||
|
||||
switch curr.Key {
|
||||
case "":
|
||||
// Don't set a merge strategy on the root unless set by user.
|
||||
// This will be handled as a special case.
|
||||
case "params":
|
||||
strategy = maps.ParamsMergeStrategyDeep
|
||||
case "outputformats", "mediatypes":
|
||||
if prevIsRoot {
|
||||
strategy = maps.ParamsMergeStrategyShallow
|
||||
}
|
||||
case "menus":
|
||||
isMenuKey := prevIsRoot
|
||||
if !isMenuKey {
|
||||
// Can also be set below languages.
|
||||
// root > languages > en > menus
|
||||
if len(params) == 4 && params[1].Key == "languages" {
|
||||
isMenuKey = true
|
||||
}
|
||||
}
|
||||
if isMenuKey {
|
||||
strategy = maps.ParamsMergeStrategyShallow
|
||||
}
|
||||
default:
|
||||
if strategy == "" {
|
||||
strategy = maps.ParamsMergeStrategyNone
|
||||
}
|
||||
}
|
||||
|
||||
return strategy
|
||||
}
|
||||
|
||||
type KeyParams struct {
|
||||
Key string
|
||||
Params maps.Params
|
||||
}
|
||||
|
||||
func (c *defaultConfigProvider) SetDefaultMergeStrategy() {
|
||||
c.WalkParams(func(params ...KeyParams) bool {
|
||||
if len(params) == 0 {
|
||||
return false
|
||||
}
|
||||
p := params[len(params)-1].Params
|
||||
var found bool
|
||||
if _, found = p.GetMergeStrategy(); found {
|
||||
// Set by user.
|
||||
return false
|
||||
}
|
||||
strategy := c.determineMergeStrategy(params...)
|
||||
if strategy != "" {
|
||||
p.SetDefaultMergeStrategy(strategy)
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func (c *defaultConfigProvider) getNestedKeyAndMap(key string, create bool) (string, maps.Params) {
|
||||
var parts []string
|
||||
v, ok := c.keyCache.Load(key)
|
||||
if ok {
|
||||
parts = v.([]string)
|
||||
} else {
|
||||
parts = strings.Split(key, ".")
|
||||
c.keyCache.Store(key, parts)
|
||||
}
|
||||
current := c.root
|
||||
for i := 0; i < len(parts)-1; i++ {
|
||||
next, found := current[parts[i]]
|
||||
if !found {
|
||||
if create {
|
||||
next = make(maps.Params)
|
||||
current[parts[i]] = next
|
||||
} else {
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
var ok bool
|
||||
current, ok = next.(maps.Params)
|
||||
if !ok {
|
||||
// E.g. a string, not a map that we can store values in.
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
return parts[len(parts)-1], current
|
||||
}
|
||||
@@ -0,0 +1,414 @@
|
||||
// Copyright 2021 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/gohugoio/hugo/common/para"
|
||||
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestDefaultConfigProvider(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
c.Run("Set and get", func(c *qt.C) {
|
||||
cfg := New()
|
||||
var k string
|
||||
var v interface{}
|
||||
|
||||
k, v = "foo", "bar"
|
||||
cfg.Set(k, v)
|
||||
c.Assert(cfg.Get(k), qt.Equals, v)
|
||||
c.Assert(cfg.Get(strings.ToUpper(k)), qt.Equals, v)
|
||||
c.Assert(cfg.GetString(k), qt.Equals, v)
|
||||
|
||||
k, v = "foo", 42
|
||||
cfg.Set(k, v)
|
||||
c.Assert(cfg.Get(k), qt.Equals, v)
|
||||
c.Assert(cfg.GetInt(k), qt.Equals, v)
|
||||
|
||||
c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{
|
||||
"foo": 42,
|
||||
})
|
||||
})
|
||||
|
||||
c.Run("Set and get map", func(c *qt.C) {
|
||||
cfg := New()
|
||||
|
||||
cfg.Set("foo", map[string]interface{}{
|
||||
"bar": "baz",
|
||||
})
|
||||
|
||||
c.Assert(cfg.Get("foo"), qt.DeepEquals, maps.Params{
|
||||
"bar": "baz",
|
||||
})
|
||||
|
||||
c.Assert(cfg.GetStringMap("foo"), qt.DeepEquals, map[string]interface{}{"bar": string("baz")})
|
||||
c.Assert(cfg.GetStringMapString("foo"), qt.DeepEquals, map[string]string{"bar": string("baz")})
|
||||
})
|
||||
|
||||
c.Run("Set and get nested", func(c *qt.C) {
|
||||
cfg := New()
|
||||
|
||||
cfg.Set("a", map[string]interface{}{
|
||||
"B": "bv",
|
||||
})
|
||||
cfg.Set("a.c", "cv")
|
||||
|
||||
c.Assert(cfg.Get("a"), qt.DeepEquals, maps.Params{
|
||||
"b": "bv",
|
||||
"c": "cv",
|
||||
})
|
||||
c.Assert(cfg.Get("a.c"), qt.Equals, "cv")
|
||||
|
||||
cfg.Set("b.a", "av")
|
||||
c.Assert(cfg.Get("b"), qt.DeepEquals, maps.Params{
|
||||
"a": "av",
|
||||
})
|
||||
|
||||
cfg.Set("b", map[string]interface{}{
|
||||
"b": "bv",
|
||||
})
|
||||
|
||||
c.Assert(cfg.Get("b"), qt.DeepEquals, maps.Params{
|
||||
"a": "av",
|
||||
"b": "bv",
|
||||
})
|
||||
|
||||
cfg = New()
|
||||
|
||||
cfg.Set("a", "av")
|
||||
|
||||
cfg.Set("", map[string]interface{}{
|
||||
"a": "av2",
|
||||
"b": "bv2",
|
||||
})
|
||||
|
||||
c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{
|
||||
"a": "av2",
|
||||
"b": "bv2",
|
||||
})
|
||||
|
||||
cfg = New()
|
||||
|
||||
cfg.Set("a", "av")
|
||||
|
||||
cfg.Set("", map[string]interface{}{
|
||||
"b": "bv2",
|
||||
})
|
||||
|
||||
c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{
|
||||
"a": "av",
|
||||
"b": "bv2",
|
||||
})
|
||||
|
||||
cfg = New()
|
||||
|
||||
cfg.Set("", map[string]interface{}{
|
||||
"foo": map[string]interface{}{
|
||||
"a": "av",
|
||||
},
|
||||
})
|
||||
|
||||
cfg.Set("", map[string]interface{}{
|
||||
"foo": map[string]interface{}{
|
||||
"b": "bv2",
|
||||
},
|
||||
})
|
||||
|
||||
c.Assert(cfg.Get("foo"), qt.DeepEquals, maps.Params{
|
||||
"a": "av",
|
||||
"b": "bv2",
|
||||
})
|
||||
})
|
||||
|
||||
c.Run("Merge default strategy", func(c *qt.C) {
|
||||
cfg := New()
|
||||
|
||||
cfg.Set("a", map[string]interface{}{
|
||||
"B": "bv",
|
||||
})
|
||||
|
||||
cfg.Merge("a", map[string]interface{}{
|
||||
"B": "bv2",
|
||||
"c": "cv2",
|
||||
})
|
||||
|
||||
c.Assert(cfg.Get("a"), qt.DeepEquals, maps.Params{
|
||||
"b": "bv",
|
||||
"c": "cv2",
|
||||
})
|
||||
|
||||
cfg = New()
|
||||
|
||||
cfg.Set("a", "av")
|
||||
|
||||
cfg.Merge("", map[string]interface{}{
|
||||
"a": "av2",
|
||||
"b": "bv2",
|
||||
})
|
||||
|
||||
c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{
|
||||
"a": "av",
|
||||
"b": "bv2",
|
||||
})
|
||||
})
|
||||
|
||||
c.Run("Merge shallow", func(c *qt.C) {
|
||||
cfg := New()
|
||||
|
||||
cfg.Set("a", map[string]interface{}{
|
||||
"_merge": "shallow",
|
||||
"B": "bv",
|
||||
"c": map[string]interface{}{
|
||||
"b": "bv",
|
||||
},
|
||||
})
|
||||
|
||||
cfg.Merge("a", map[string]interface{}{
|
||||
"c": map[string]interface{}{
|
||||
"d": "dv2",
|
||||
},
|
||||
"e": "ev2",
|
||||
})
|
||||
|
||||
c.Assert(cfg.Get("a"), qt.DeepEquals, maps.Params{
|
||||
"e": "ev2",
|
||||
"_merge": maps.ParamsMergeStrategyShallow,
|
||||
"b": "bv",
|
||||
"c": maps.Params{
|
||||
"b": "bv",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
// Issue #8679
|
||||
c.Run("Merge typed maps", func(c *qt.C) {
|
||||
|
||||
for _, left := range []interface{}{
|
||||
map[string]string{
|
||||
"c": "cv1",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"c": "cv1",
|
||||
},
|
||||
map[interface{}]interface{}{
|
||||
"c": "cv1",
|
||||
},
|
||||
} {
|
||||
cfg := New()
|
||||
|
||||
cfg.Set("", map[string]interface{}{
|
||||
"b": left,
|
||||
})
|
||||
|
||||
cfg.Merge("", maps.Params{
|
||||
"b": maps.Params{
|
||||
"c": "cv2",
|
||||
"d": "dv2",
|
||||
},
|
||||
})
|
||||
|
||||
c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{
|
||||
"b": maps.Params{
|
||||
"c": "cv1",
|
||||
"d": "dv2",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
for _, left := range []interface{}{
|
||||
map[string]string{
|
||||
"b": "bv1",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"b": "bv1",
|
||||
},
|
||||
map[interface{}]interface{}{
|
||||
"b": "bv1",
|
||||
},
|
||||
} {
|
||||
|
||||
for _, right := range []interface{}{
|
||||
map[string]string{
|
||||
"b": "bv2",
|
||||
"c": "cv2",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"b": "bv2",
|
||||
"c": "cv2",
|
||||
},
|
||||
map[interface{}]interface{}{
|
||||
"b": "bv2",
|
||||
"c": "cv2",
|
||||
},
|
||||
} {
|
||||
cfg := New()
|
||||
|
||||
cfg.Set("a", left)
|
||||
|
||||
cfg.Merge("a", right)
|
||||
|
||||
c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{
|
||||
"a": maps.Params{
|
||||
"b": "bv1",
|
||||
"c": "cv2",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
// Issue #8701
|
||||
c.Run("Prevent _merge only maps", func(c *qt.C) {
|
||||
cfg := New()
|
||||
|
||||
cfg.Set("", map[string]interface{}{
|
||||
"B": "bv",
|
||||
})
|
||||
|
||||
cfg.Merge("", map[string]interface{}{
|
||||
"c": map[string]interface{}{
|
||||
"_merge": "shallow",
|
||||
"d": "dv2",
|
||||
},
|
||||
})
|
||||
|
||||
c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{
|
||||
"b": "bv",
|
||||
})
|
||||
})
|
||||
|
||||
c.Run("IsSet", func(c *qt.C) {
|
||||
cfg := New()
|
||||
|
||||
cfg.Set("a", map[string]interface{}{
|
||||
"B": "bv",
|
||||
})
|
||||
|
||||
c.Assert(cfg.IsSet("A"), qt.IsTrue)
|
||||
c.Assert(cfg.IsSet("a.b"), qt.IsTrue)
|
||||
c.Assert(cfg.IsSet("z"), qt.IsFalse)
|
||||
})
|
||||
|
||||
c.Run("Para", func(c *qt.C) {
|
||||
cfg := New()
|
||||
p := para.New(4)
|
||||
r, _ := p.Start(context.Background())
|
||||
|
||||
setAndGet := func(k string, v int) error {
|
||||
vs := strconv.Itoa(v)
|
||||
cfg.Set(k, v)
|
||||
err := errors.New("get failed")
|
||||
if cfg.Get(k) != v {
|
||||
return err
|
||||
}
|
||||
if cfg.GetInt(k) != v {
|
||||
return err
|
||||
}
|
||||
if cfg.GetString(k) != vs {
|
||||
return err
|
||||
}
|
||||
if !cfg.IsSet(k) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
for i := 0; i < 20; i++ {
|
||||
i := i
|
||||
r.Run(func() error {
|
||||
const v = 42
|
||||
k := fmt.Sprintf("k%d", i)
|
||||
if err := setAndGet(k, v); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m := maps.Params{
|
||||
"new": 42,
|
||||
}
|
||||
|
||||
cfg.Merge("", m)
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
c.Assert(r.Wait(), qt.IsNil)
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkDefaultConfigProvider(b *testing.B) {
|
||||
type cfger interface {
|
||||
Get(key string) interface{}
|
||||
Set(key string, value interface{})
|
||||
IsSet(key string) bool
|
||||
}
|
||||
|
||||
newMap := func() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"a": map[string]interface{}{
|
||||
"b": map[string]interface{}{
|
||||
"c": 32,
|
||||
"d": 43,
|
||||
},
|
||||
},
|
||||
"b": 62,
|
||||
}
|
||||
}
|
||||
|
||||
runMethods := func(b *testing.B, cfg cfger) {
|
||||
m := newMap()
|
||||
cfg.Set("mymap", m)
|
||||
cfg.Set("num", 32)
|
||||
if !(cfg.IsSet("mymap") && cfg.IsSet("mymap.a") && cfg.IsSet("mymap.a.b") && cfg.IsSet("mymap.a.b.c")) {
|
||||
b.Fatal("IsSet failed")
|
||||
}
|
||||
|
||||
if cfg.Get("num") != 32 {
|
||||
b.Fatal("Get failed")
|
||||
}
|
||||
|
||||
if cfg.Get("mymap.a.b.c") != 32 {
|
||||
b.Fatal("Get failed")
|
||||
}
|
||||
}
|
||||
|
||||
b.Run("Viper", func(b *testing.B) {
|
||||
v := viper.New()
|
||||
for i := 0; i < b.N; i++ {
|
||||
runMethods(b, v)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Custom", func(b *testing.B) {
|
||||
cfg := New()
|
||||
for i := 0; i < b.N; i++ {
|
||||
runMethods(b, cfg)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright 2021 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/docshelper"
|
||||
)
|
||||
|
||||
// This is is just some helpers used to create some JSON used in the Hugo docs.
|
||||
func init() {
|
||||
docsProvider := func() docshelper.DocProvider {
|
||||
|
||||
cfg := New()
|
||||
for _, configRoot := range ConfigRootKeys {
|
||||
cfg.Set(configRoot, make(maps.Params))
|
||||
}
|
||||
lang := maps.Params{
|
||||
"en": maps.Params{
|
||||
"menus": maps.Params{},
|
||||
"params": maps.Params{},
|
||||
},
|
||||
}
|
||||
cfg.Set("languages", lang)
|
||||
cfg.SetDefaultMergeStrategy()
|
||||
|
||||
configHelpers := map[string]interface{}{
|
||||
"mergeStrategy": cfg.Get(""),
|
||||
}
|
||||
return docshelper.DocProvider{"config": configHelpers}
|
||||
}
|
||||
|
||||
docshelper.AddDocProviderFunc(docsProvider)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user