mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-25 15:58:53 +00:00
Compare commits
679 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 45f4eb9846 | |||
| 263f86d456 | |||
| bed75108e4 | |||
| 8d7006d853 | |||
| e5b6e2085a | |||
| 77bf2991b1 | |||
| e5f2299741 | |||
| 87a07282a2 | |||
| 882d678bbf | |||
| fa28df1058 | |||
| 072aa7f110 | |||
| 215d2ed883 | |||
| 23adc0c2d9 | |||
| 461b0e066f | |||
| 5cb4e46cfa | |||
| 9f5a92078a | |||
| 47953148b6 | |||
| 2fc0abd22a | |||
| 53da3881de | |||
| ac101aba4f | |||
| 58a47ccde0 | |||
| 95b1d3013b | |||
| a5604e18b0 | |||
| 05d0eddd2b | |||
| 35febb2e2a | |||
| c624a77992 | |||
| 020086cb2b | |||
| 8524baee16 | |||
| 90b0127f63 | |||
| 7611078dae | |||
| b2a3d4644b | |||
| 88c8a15be1 | |||
| 59c4bc52ed | |||
| dc1d4a9200 | |||
| de7b9475c0 | |||
| d6ca728ff3 | |||
| ff10aa522d | |||
| 9f258d2b8f | |||
| 31c9c690fd | |||
| e8a716b23a | |||
| 3e6cb2cb77 | |||
| 8d898ad667 | |||
| fad183c4ae | |||
| b6867bf806 | |||
| 9df57154ee | |||
| 35abce27ca | |||
| 8914fe7ed7 | |||
| b0f536fb27 | |||
| 1384d77a04 | |||
| 5e83f42562 | |||
| b376b2685a | |||
| fb007e9ae5 | |||
| 4c560020bc | |||
| d44d3ea898 | |||
| d1cf53f5f4 | |||
| 811ee996a6 | |||
| 4f61a926f6 | |||
| be04ece859 | |||
| 5b4b8bb3c1 | |||
| 2278b0eb02 | |||
| 41974303f3 | |||
| 6b3f1a1002 | |||
| fa8e334dae | |||
| a5d4c82d2f | |||
| 32f4b38338 | |||
| 9b48c5d6bd | |||
| 8a1873a2d5 | |||
| 95ce2a40e7 | |||
| e22b3f54c3 | |||
| 71b8d8b6a4 | |||
| 5dc6d0df94 | |||
| 527cf1ab03 | |||
| f4956d9aae | |||
| 2838d58b1d | |||
| f330e869e4 | |||
| f2b5d4de34 | |||
| 0091b1f893 | |||
| c7165589b3 | |||
| ad5703a917 | |||
| f94a388ad3 | |||
| a83256b9cd | |||
| 77ad1336d2 | |||
| e33ed29b75 | |||
| 2fb708d3d0 | |||
| cee181c3a6 | |||
| b1d438ea28 | |||
| f147b5c53e | |||
| 6b76841b05 | |||
| bcbed4ebda | |||
| 009076e5ee | |||
| 1cbb501be8 | |||
| 66b143a01d | |||
| 4f93f8c670 | |||
| f76e50118b | |||
| f84df16fea | |||
| 579004178b | |||
| f2fbca23aa | |||
| 1707f1a5f5 | |||
| 211d667979 | |||
| 33c738116c | |||
| 69a56420ae | |||
| 4756ec3cd8 | |||
| 4c3c512038 | |||
| 0775c98e6c | |||
| 1477fb33c9 | |||
| 993b84333c | |||
| a9ee2ab58c | |||
| c85b726f8a | |||
| 89cf7a3f4b | |||
| 75b16e30ec | |||
| 0508ca1856 | |||
| a0c28c943c | |||
| c7dd66bfe2 | |||
| 7aeeb60d7e | |||
| ea529c847e | |||
| 6c80acbd5e | |||
| 7fbfedf013 | |||
| 06f56fc983 | |||
| d7a67dcb51 | |||
| 3e421bd47c | |||
| 1028760331 | |||
| 9d02034881 | |||
| ccfbb63d17 | |||
| fcd63a865e | |||
| 7ace59d5bf | |||
| 35f41834ea | |||
| 9b17cbb62a | |||
| 2957795f52 | |||
| 56550d1e44 | |||
| 7881b0965f | |||
| 49d0a82641 | |||
| b799b12f4a | |||
| 701486728e | |||
| f2795d4d2c | |||
| e85c057f99 | |||
| 223b3c2eec | |||
| 7ca15c7ddb | |||
| adb776b22b | |||
| 5efdd14ebd | |||
| 8d7607aed1 | |||
| 4d425a86f5 | |||
| 9e9a1f92ba | |||
| f7375c4972 | |||
| 7966c0b5b7 | |||
| cc98fc8ad2 | |||
| 8b73644d81 | |||
| 3b86b4a9f5 | |||
| 27a8049da7 | |||
| ee4274244b | |||
| 4333cc77fe | |||
| 452cdcd7c8 | |||
| 05d614e13b | |||
| 018cd0c9da | |||
| 612a06f067 | |||
| 3db4a1cf7a | |||
| f34e6172cf | |||
| 708d4ceebd | |||
| ed65bda3b4 | |||
| 3a62d54745 | |||
| ebab291c0e | |||
| 75467cd785 | |||
| d1553b4b0f | |||
| 1d9dde82a0 | |||
| 87b16abd93 | |||
| 415ca9673d | |||
| 8559f5c29f | |||
| e2dc432fe2 | |||
| 950295516d | |||
| 032e6802d1 | |||
| d0d661dffd | |||
| 4494a01b79 | |||
| e91e222cd2 | |||
| 5185fb065b | |||
| 92baa14fd3 | |||
| a55640de8e | |||
| 9225db636e | |||
| f7ef7d3761 | |||
| f9d6feca08 | |||
| 439ab0339d | |||
| b6a60f718e | |||
| bfdc44964a | |||
| cc8515f187 | |||
| 8bfd3a54a4 | |||
| bb533ca5e1 | |||
| 876e5f67b1 | |||
| c2037f0c9a | |||
| 9bc6187b83 | |||
| 10bb614a70 | |||
| bceda1b288 | |||
| 4dae52af68 | |||
| 794d4052b8 | |||
| a7ee9b0bb9 | |||
| 91ef9655aa | |||
| b5f39d23b8 | |||
| 3011f36c27 | |||
| d30e845485 | |||
| 597e418cb0 | |||
| 44f5c1c14c | |||
| 984a73af9e | |||
| e54213f525 | |||
| 4a2a8afff2 | |||
| 79d517d86c | |||
| 34c49d788c | |||
| b9e75afd6c | |||
| dfc72d61a5 | |||
| 02eaddc2fb | |||
| bdf47e8da8 | |||
| 75904332f3 | |||
| 60c0eb4e89 | |||
| c154c2f7b2 | |||
| 075b17ee1d | |||
| b4148cd1d9 | |||
| 908692fae5 | |||
| 7e4b18c5ae | |||
| 483cf19d5d | |||
| 7201042946 | |||
| 5383fe458c | |||
| b1a82c61ab | |||
| 6fe6b9c17f | |||
| 52cec0fb2d | |||
| 23e52ea4c8 | |||
| 9b619dc023 | |||
| fab41f42d3 | |||
| f27faf9afd | |||
| c7c66e664a | |||
| 5e078383a7 | |||
| ddc15ed41b | |||
| ddc6d4e30f | |||
| 6a2bfcbec8 | |||
| 59d87044a4 | |||
| 3244cb3b31 | |||
| d9282cf98a | |||
| c52045bbb3 | |||
| 8ed2a1caa9 | |||
| e85961390a | |||
| 2a9060a85c | |||
| 3038464ea6 | |||
| 526b5b1c49 | |||
| 26f75edb7a | |||
| e1a66c7343 | |||
| db3c49d049 | |||
| e3cb8e6c78 | |||
| 55251aa890 | |||
| 843fcd19d4 | |||
| ce8a09a4c0 | |||
| 669ada4367 | |||
| 507451229c | |||
| 40ffb0484b | |||
| 4bae8b04aa | |||
| 0584432b07 | |||
| 65fa069271 | |||
| 9e4f9e0bb6 | |||
| 3f3187de0f | |||
| cc351958e1 | |||
| c24f3ae22b | |||
| 214e9745b6 | |||
| ecc8922752 | |||
| 22afe28047 | |||
| 978856e2ad | |||
| 8431871201 | |||
| 8fc339dc25 | |||
| 5236cb8e9a | |||
| a28865cfc3 | |||
| cb6c77b813 | |||
| be58c7b9c8 | |||
| 62d031aedf | |||
| ce06bdb16a | |||
| 2efc1a64c3 | |||
| 25ddbb09fe | |||
| e691c48a5a | |||
| 094709e105 | |||
| a574469797 | |||
| 822dc627a1 | |||
| 43f9df0194 | |||
| 1021714449 | |||
| 5a83bf314f | |||
| 6f069e549b | |||
| 0483299bc0 | |||
| 27b62a546c | |||
| 35bfca3b14 | |||
| 9cd54cab20 | |||
| a8853f1c5a | |||
| 4d93aca27d | |||
| f2167de834 | |||
| 64b6b29075 | |||
| bb9c2988f8 | |||
| c04030b14c | |||
| f7691fe965 | |||
| efe0b4e5c0 | |||
| d5a0b6bbbc | |||
| 5178cd13a7 | |||
| 9ce0a1fb70 | |||
| ab9214768d | |||
| 5068681707 | |||
| b17a61a605 | |||
| eb16165694 | |||
| 7829474088 | |||
| 256418917c | |||
| c84f506f8e | |||
| 4b5f743959 | |||
| 931a132450 | |||
| 8378358857 | |||
| 831d23cb4d | |||
| 514e18dc27 | |||
| b804a70881 | |||
| 30a7c9ea37 | |||
| 8346848109 | |||
| 54ecade02f | |||
| 9433cf5a92 | |||
| 3c909eb3f3 | |||
| 724b3e673b | |||
| 056c754771 | |||
| a9a93d082d | |||
| b09a40333f | |||
| 7540a62834 | |||
| 7e75aeca80 | |||
| e14e0b192f | |||
| ed698e94c1 | |||
| 89e2716d29 | |||
| aba2647c15 | |||
| f37c5a2567 | |||
| aded0f25fd | |||
| bc337e6ab5 | |||
| 112461fded | |||
| 94ab125b27 | |||
| dcfeed35c6 | |||
| f9b4eb4f39 | |||
| e82b2dc8c1 | |||
| 5df2b79dd2 | |||
| 12742bac71 | |||
| fabf026f49 | |||
| 94f0f7e597 | |||
| 3c29c5af8e | |||
| 33502667fb | |||
| d3489eba5d | |||
| 17d7ecde2b | |||
| e4b2572880 | |||
| 782dd15858 | |||
| aff9c09166 | |||
| fdd4a768f0 | |||
| abeeff1325 | |||
| f7aeaa6129 | |||
| 7d78a2afd3 | |||
| 5570a6e478 | |||
| c0b3a1af03 | |||
| a8cb1b07b4 | |||
| 2900801acf | |||
| f3d5190793 | |||
| 1ccc0b79a2 | |||
| cbed50e22b | |||
| 05ecd8111c | |||
| e456e34bdb | |||
| 5d14d04ac6 | |||
| ddcb402859 | |||
| 3a44920e79 | |||
| 79a06aa4b6 | |||
| d212f60949 | |||
| 488776b649 | |||
| b8b8436fcc | |||
| 42d8dfc8c8 | |||
| 47506d1644 | |||
| 2998fa0cd5 | |||
| d16a7a33ff | |||
| 5b1edd281a | |||
| 2bd9d9099d | |||
| 4b7d3e57a4 | |||
| 4f3c095963 | |||
| 486bc46a52 | |||
| 61f210dd7a | |||
| f8446188db | |||
| d523aa4bb0 | |||
| b8725f5181 | |||
| 0bc4b0246d | |||
| faeb55c1d8 | |||
| 6c6a6c87ec | |||
| 1d18eb0574 | |||
| 33a7b36fd4 | |||
| 6180c85fb8 | |||
| d3a98325c3 | |||
| b2a676f5f0 | |||
| 3a786a248d | |||
| 729593c842 | |||
| e65268f2c2 | |||
| f863b6a07e | |||
| f5be599204 | |||
| b14c11b0e1 | |||
| 0addb2add6 | |||
| 8bba85601f | |||
| 7082a5d143 | |||
| aa281b5135 | |||
| 3250198724 | |||
| d6a4af7018 | |||
| 27569275b1 | |||
| 4b2738d871 | |||
| 74309fe569 | |||
| 9c88a8a55a | |||
| 95e72f5e8e | |||
| 1c7b7b4ef2 | |||
| 1ad117cbe2 | |||
| dac7092a9c | |||
| b27ccf34bf | |||
| 286d0b7dc3 | |||
| 83c873ff37 | |||
| a2440dc0e2 | |||
| 1e9ac3dcc2 | |||
| 40e99672b6 | |||
| 3a3badfd1d | |||
| df021317a9 | |||
| 78578632f5 | |||
| 6b78b3810a | |||
| 78a4c2e32e | |||
| d4ebfea1ff | |||
| 93aa6261b4 | |||
| acc14b4646 | |||
| 2d7709d155 | |||
| deff9e154b | |||
| 889aca054a | |||
| e3ed4a83b8 | |||
| 6636cf1bea | |||
| f669ef6bec | |||
| ed7b3e2619 | |||
| 2bf686ee21 | |||
| 9f74dc2a52 | |||
| d1661b823a | |||
| 7930d2132a | |||
| eb038cfa0a | |||
| 129c27ee6e | |||
| 44da60d869 | |||
| 1e3e34002d | |||
| 1b7ecfc2e1 | |||
| 27f5a906a2 | |||
| 2fdc4a24d5 | |||
| f6863e1ef7 | |||
| d6c16afde0 | |||
| 6f3716dc22 | |||
| 5a52cd5f92 | |||
| 4a366fcfee | |||
| 083311d033 | |||
| 0fe4ff1875 | |||
| 1f42e47e47 | |||
| 35fbfb19a1 | |||
| 3a3089121b | |||
| c21e5179ce | |||
| a205f24ba5 | |||
| 92979d9288 | |||
| 0a3340e952 | |||
| 6b21ac3e67 | |||
| d14420539a | |||
| 3d4a9882bf | |||
| bdca972794 | |||
| 604ddb90c5 | |||
| b09e495a71 | |||
| 42cde66663 | |||
| ad60948c46 | |||
| c397f2c080 | |||
| b9e18e20c8 | |||
| 2159d77f36 | |||
| 535755e4f8 | |||
| 3583dd6d71 | |||
| e5b4cb2419 | |||
| 821adf3ae8 | |||
| 8ba9a27366 | |||
| 235acf2232 | |||
| ac0fac9347 | |||
| 23f48c300c | |||
| e2201ef15f | |||
| b5e17f7c83 | |||
| 31a8bb8c07 | |||
| 8e825ddf5b | |||
| 646a52a5c5 | |||
| e421696d02 | |||
| 4b4af2c52e | |||
| fae48d7457 | |||
| 06d28a464d | |||
| 6818170308 | |||
| c5279064df | |||
| 152cffb13a | |||
| 0d5110d033 | |||
| d3b81ee58e | |||
| 1fd30d462e | |||
| 498d629958 | |||
| 10ac2ec446 | |||
| ce264b936c | |||
| 91f49c0700 | |||
| a475bf125c | |||
| f1dede3708 | |||
| fa873a6cb3 | |||
| fb732d5322 | |||
| ad705aac06 | |||
| 52ac85fbc4 | |||
| 048a64be1c | |||
| 3033a9a37e | |||
| 27d4211187 | |||
| 4730a78a76 | |||
| b7706d6aec | |||
| 48413d76f4 | |||
| 66b9e2bff2 | |||
| 398996e8b0 | |||
| 7f005f5e98 | |||
| 011e3aaa9e | |||
| 3697448450 | |||
| 2638b04c62 | |||
| cf47f43ff1 | |||
| 3b6bd1210a | |||
| 8388cd90e8 | |||
| 807c551922 | |||
| bc57f5c36e | |||
| fe6a6f2737 | |||
| 2650fa772b | |||
| ef525b15d4 | |||
| f916814697 | |||
| cae07ce84b | |||
| 4f9c109dc5 | |||
| f1a00b2069 | |||
| df4cbbd3bd | |||
| 2e2e34a935 | |||
| 2eed35c826 | |||
| dd692c710a | |||
| ed4f1edbd7 | |||
| 058cc6c2c3 | |||
| 4b82f74848 | |||
| 06976ebb87 | |||
| 555a5612b2 | |||
| 75e54345f9 | |||
| df50c108ba | |||
| e27fd4c1b8 | |||
| b7ca3e1b3a | |||
| 13e64d7276 | |||
| 9b26b5487b | |||
| 3dafe206e3 | |||
| 37d6463479 | |||
| 43d446522a | |||
| 4f72e79120 | |||
| fe6676c775 | |||
| 7a97d3e6bc | |||
| 6667c6d743 | |||
| cfda13b363 | |||
| bb2fe814c2 | |||
| e5d66074ce | |||
| d970327d7b | |||
| 0013bea901 | |||
| 30bc4ed0a0 | |||
| f6f22ad944 | |||
| 400fe96aee | |||
| 3f45e729f4 | |||
| ffaa73dc8a | |||
| f8d8c85428 | |||
| 10dc87bf86 | |||
| 7231869ba8 | |||
| ccd3285484 | |||
| 600047ff1c | |||
| 5f2e1cb896 | |||
| c8ce65046d | |||
| a53f962312 | |||
| daffeec30d | |||
| b8b91f5506 | |||
| f0effac804 | |||
| be3ae3ec92 | |||
| 2cf8fe2ea2 | |||
| 3b103cb7b7 | |||
| 0cc936583b | |||
| c15c7da42a | |||
| bcbe57c6e9 | |||
| e38e881248 | |||
| 0665a3951b | |||
| 1ed8c36336 | |||
| e03eb90a36 | |||
| c915d0d325 | |||
| 47d4edce60 | |||
| ea8ef573c6 | |||
| 293e12355d | |||
| fdf3c3b823 | |||
| 312d2252be | |||
| f627903efa | |||
| a2f0f9d475 | |||
| 1ff54ab3d9 | |||
| 456f5476cf | |||
| 09811d0bee | |||
| d351ac2e69 | |||
| 59f57900a4 | |||
| 59cdad325a | |||
| 6022f219f1 | |||
| 0c8a415483 | |||
| f4675fa0f0 | |||
| ebb56e8bdb | |||
| 6b9934a266 | |||
| c7f057797c | |||
| 08d14113b6 | |||
| 94d6d6780f | |||
| 45c9c45d1d | |||
| fce32c07fb | |||
| d32ff16fd6 | |||
| e6eda2a370 | |||
| 66f688f712 | |||
| 2c934be8ab | |||
| 94d0e79d33 | |||
| b9a503febb | |||
| fff132537b | |||
| 8999de193c | |||
| a81c64e43e | |||
| 7ef5096257 | |||
| 16b691a875 | |||
| aefd9a80a6 | |||
| 57ace857aa | |||
| fdff0d3af4 | |||
| 834617f9f8 | |||
| ef20ec1fba | |||
| 5a0ee2b934 | |||
| 6746826689 | |||
| caf608d566 | |||
| 6711dacded | |||
| de23293486 | |||
| 918177ac25 | |||
| 83a980d290 | |||
| 4a16b5f4b0 | |||
| a95896878f | |||
| 873f8805cb | |||
| 3743875778 | |||
| baa62d0abb | |||
| 78c99463fd | |||
| abc54080ec | |||
| a655e00d70 | |||
| 9d973004f5 | |||
| 2247546017 | |||
| d139a037d9 | |||
| 0a88741fe8 | |||
| d999b4e82b | |||
| 20148345aa | |||
| e5052f4e09 | |||
| c81fbf4625 | |||
| a6f199f7a6 | |||
| 0dd06bdac0 | |||
| 5c5384916e | |||
| 56c61559b2 | |||
| 78f8475a05 | |||
| c09ee78fd2 | |||
| f6ae436c58 | |||
| 2182ecfd34 | |||
| e85833d868 | |||
| 43a5aaa778 | |||
| 1639fd20d8 | |||
| 3a44bf182f | |||
| c2a6741394 | |||
| 06bd013641 | |||
| 755d1ffe7a | |||
| fb3cb05cc3 | |||
| d07882dfb7 | |||
| 7f5356717d | |||
| 789ef8c639 | |||
| 71931b30b1 | |||
| d40116e5f9 | |||
| c362634b7d | |||
| d71120852a | |||
| a6b1eb1e91 | |||
| 27110133ff | |||
| 31faf98ce3 | |||
| f14d773841 | |||
| 3aa838cf2b | |||
| a3a26597cf | |||
| 92d776b0df | |||
| e6dd54943f | |||
| 766085c2dc | |||
| b5d13ca16b | |||
| 6b02f5c0f4 | |||
| 88e447c449 | |||
| b718d743b7 | |||
| 0ba19c57f1 | |||
| 0afa2897a0 | |||
| f219ac09f6 | |||
| 786f72302f | |||
| 0cae1cf828 | |||
| 3b18042ee6 | |||
| e38843f4bf | |||
| 87f0dc14ab | |||
| b7d9ab136d | |||
| 04d4c08dbc | |||
| 016dd4a69a | |||
| c6b599a06d | |||
| 3a6784b162 |
@@ -1,8 +1,9 @@
|
||||
defaults: &defaults
|
||||
working_directory: /go/src/github.com/gohugoio
|
||||
docker:
|
||||
- image: bepsays/ci-goreleaser:0.34.2-11
|
||||
|
||||
- image: bepsays/ci-goreleaser:1.12-5
|
||||
environment:
|
||||
CGO_ENABLED: "0"
|
||||
|
||||
version: 2
|
||||
jobs:
|
||||
build:
|
||||
@@ -12,11 +13,11 @@ jobs:
|
||||
path: hugo
|
||||
- run:
|
||||
command: |
|
||||
go get -d github.com/magefile/mage/...
|
||||
git clone git@github.com:gohugoio/hugoDocs.git
|
||||
cd hugo
|
||||
mage vendor
|
||||
mage check
|
||||
go mod download
|
||||
sleep 5
|
||||
go test -p 1 ./...
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths: .
|
||||
@@ -24,7 +25,7 @@ jobs:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: /go/src/github.com/gohugoio
|
||||
at: /root/project
|
||||
- run:
|
||||
command: |
|
||||
cd hugo
|
||||
|
||||
@@ -6,3 +6,4 @@
|
||||
.circleci
|
||||
docs
|
||||
examples
|
||||
Dockerfile
|
||||
|
||||
+5
-1
@@ -1,4 +1,4 @@
|
||||
hugo
|
||||
/hugo
|
||||
docs/public*
|
||||
/.idea
|
||||
hugo.exe
|
||||
@@ -19,3 +19,7 @@ dock.sh
|
||||
|
||||
GoBuilds
|
||||
dist
|
||||
|
||||
resources/sunset.jpg
|
||||
|
||||
vendor
|
||||
+21
-5
@@ -1,30 +1,46 @@
|
||||
language: go
|
||||
sudo: false
|
||||
dist: trusty
|
||||
dist: xenial
|
||||
env:
|
||||
HUGO_BUILD_TAGS="extended"
|
||||
global:
|
||||
- GOPROXY="https://proxy.golang.org"
|
||||
- HUGO_BUILD_TAGS="extended"
|
||||
git:
|
||||
depth: false
|
||||
go:
|
||||
- 1.9.7
|
||||
- "1.10.3"
|
||||
- "1.11.10"
|
||||
- "1.12.5"
|
||||
- tip
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
- windows
|
||||
matrix:
|
||||
allow_failures:
|
||||
- go: tip
|
||||
fast_finish: true
|
||||
exclude:
|
||||
- os: windows
|
||||
go: tip
|
||||
|
||||
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
|
||||
- mage -v vendor
|
||||
script:
|
||||
- go mod download || true
|
||||
- mage -v test
|
||||
- mage -v check
|
||||
- mage -v hugo
|
||||
- ./hugo -s docs/
|
||||
- ./hugo --renderToMemory -s docs/
|
||||
- df -h
|
||||
|
||||
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
|
||||
|
||||
+17
-37
@@ -18,11 +18,8 @@ The Hugo community and maintainers are [very active](https://github.com/gohugoio
|
||||
* [Submitting Patches](#submitting-patches)
|
||||
* [Code Contribution Guidelines](#code-contribution-guidelines)
|
||||
* [Git Commit Message Guidelines](#git-commit-message-guidelines)
|
||||
* [Vendored Dependencies](#vendored-dependencies)
|
||||
* [Fetching the Sources From GitHub](#fetching-the-sources-from-github)
|
||||
* [Using Git Remotes](#using-git-remotes)
|
||||
* [Build Hugo with Your Changes](#build-hugo-with-your-changes)
|
||||
* [Updating the Hugo Sources](#updating-the-hugo-sources)
|
||||
* [Building Hugo with Your Changes](#building-hugo-with-your-changes)
|
||||
|
||||
## Asking Support Questions
|
||||
|
||||
@@ -73,7 +70,7 @@ To make the contribution process as seamless as possible, we ask for the followi
|
||||
* Run `go fmt`.
|
||||
* Add documentation if you are adding new features or changing functionality. The docs site lives in `/docs`.
|
||||
* Squash your commits into a single commit. `git rebase -i`. It’s okay to force update your pull request with `git push -f`.
|
||||
* Ensure that `mage check` succeeds. [Travis CI](https://travis-ci.org/gohugoio/hugo) (Linux and macOS) and [AppVeyor](https://ci.appveyor.com/project/gohugoio/hugo/branch/master) (Windows) will fail the build if `mage check` fails.
|
||||
* Ensure that `mage check` succeeds. [Travis CI](https://travis-ci.org/gohugoio/hugo) (Windows, Linux and macOS) will fail the build if `mage check` fails.
|
||||
* Follow the **Git Commit Message Guidelines** below.
|
||||
|
||||
### Git Commit Message Guidelines
|
||||
@@ -106,30 +103,25 @@ Fixes #1949
|
||||
|
||||
### Fetching the Sources From GitHub
|
||||
|
||||
Due to the way Go handles package imports, the best approach for working on a
|
||||
Hugo fork is to use Git Remotes. Here's a simple walk-through for getting
|
||||
started:
|
||||
Since Hugo 0.48, Hugo uses the Go Modules support built into Go 1.11 to build. The easiest is to clone Hugo in a directory outside of `GOPATH`, as in the following example:
|
||||
|
||||
1. Get the Hugo source:
|
||||
```bash
|
||||
mkdir $HOME/src
|
||||
cd $HOME/src
|
||||
git clone https://github.com/gohugoio/hugo.git
|
||||
cd hugo
|
||||
go install
|
||||
```
|
||||
|
||||
```bash
|
||||
go get -u -v -d github.com/gohugoio/hugo
|
||||
```
|
||||
>Note: Some Go tools may not be fully updated to support Go Modules yet. One example would be LiteIDE. Follow [this workaround](https://github.com/visualfc/liteide/issues/986#issuecomment-428117702) for how to continue to work with Hugo below `GOPATH`.
|
||||
|
||||
1. Install Mage:
|
||||
For some convenient build and test targets, you also will want to install Mage:
|
||||
|
||||
```bash
|
||||
go get github.com/magefile/mage
|
||||
```
|
||||
```bash
|
||||
go get github.com/magefile/mage
|
||||
```
|
||||
|
||||
1. Change to the Hugo source directory and fetch the dependencies:
|
||||
|
||||
```bash
|
||||
cd $HOME/go/src/github.com/gohugoio/hugo
|
||||
mage vendor
|
||||
```
|
||||
|
||||
Note that Hugo uses [Go Dep](https://github.com/golang/dep) to vendor dependencies, rather than a simple `go get`. We don't commit the vendored packages themselves to the Hugo git repository. The call to `mage vendor` takes care of all this for you.
|
||||
Now, to make a change to Hugo's source:
|
||||
|
||||
1. Create a new branch for your changes (the branch name is arbitrary):
|
||||
|
||||
@@ -167,7 +159,7 @@ Hugo uses [mage](https://github.com/magefile/mage) to sync vendor dependencies,
|
||||
cd $HOME/go/src/github.com/gohugoio/hugo
|
||||
```
|
||||
|
||||
To build Hugo:
|
||||
To build Hugo:
|
||||
|
||||
```bash
|
||||
mage hugo
|
||||
@@ -197,15 +189,3 @@ mage -l
|
||||
```bash
|
||||
HUGO_BUILD_TAGS=extended mage install
|
||||
````
|
||||
|
||||
### Updating the Hugo Sources
|
||||
|
||||
If you want to stay in sync with the Hugo repository, you can easily pull down
|
||||
the source changes, but you'll need to keep the vendored packages up-to-date as
|
||||
well.
|
||||
|
||||
```bash
|
||||
git pull
|
||||
mage vendor
|
||||
```
|
||||
|
||||
|
||||
+18
-12
@@ -2,26 +2,32 @@
|
||||
# Twitter: https://twitter.com/gohugoio
|
||||
# Website: https://gohugo.io/
|
||||
|
||||
FROM golang:1.10.3-alpine3.7 AS build
|
||||
FROM golang:1.11-stretch AS build
|
||||
|
||||
ENV CGO_ENABLED=0
|
||||
ENV GOOS=linux
|
||||
|
||||
WORKDIR /go/src/github.com/gohugoio/hugo
|
||||
RUN apk add --no-cache \
|
||||
git \
|
||||
musl-dev && \
|
||||
go get github.com/golang/dep/cmd/dep
|
||||
RUN apt-get install \
|
||||
git gcc g++ binutils
|
||||
COPY . /go/src/github.com/gohugoio/hugo/
|
||||
RUN dep ensure -vendor-only && \
|
||||
go install -ldflags '-s -w'
|
||||
ENV GO111MODULE=on
|
||||
RUN go get -d .
|
||||
|
||||
ARG CGO=0
|
||||
ENV CGO_ENABLED=${CGO}
|
||||
ENV GOOS=linux
|
||||
|
||||
# default non-existent build tag so -tags always has an arg
|
||||
ARG BUILD_TAGS="99notag"
|
||||
RUN go install -ldflags '-w -extldflags "-static"' -tags ${BUILD_TAGS}
|
||||
|
||||
# ---
|
||||
|
||||
FROM scratch
|
||||
FROM alpine:3.9
|
||||
RUN apk add --no-cache ca-certificates
|
||||
COPY --from=build /go/bin/hugo /hugo
|
||||
WORKDIR /site
|
||||
VOLUME /site
|
||||
ARG WORKDIR="/site"
|
||||
WORKDIR ${WORKDIR}
|
||||
VOLUME ${WORKDIR}
|
||||
EXPOSE 1313
|
||||
ENTRYPOINT [ "/hugo" ]
|
||||
CMD [ "--help" ]
|
||||
|
||||
Generated
-714
@@ -1,714 +0,0 @@
|
||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:f6a10073544cc0bd1eb9fd9f8d9bf4644971910a0f8393d51b5b4d286e554849"
|
||||
name = "github.com/BurntSushi/locker"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "a6e239ea1c69bff1cfdb20c4b73dadf52f784b6a"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:af3600b068c8dd0e004122f154de7ece06b0b13376f79e976701aab4834c860a"
|
||||
name = "github.com/BurntSushi/toml"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "a368813c5e648fee92e5f6c30e3944ff9d5e8895"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:1080c443bebc98b1c25665b46b321dc02f8a4d384fe544379dcba1e651f59321"
|
||||
name = "github.com/PuerkitoBio/purell"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "0bcb03f4b4d0a9428594752bd2a3b9aa0a9d4bd4"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:331a419049c2be691e5ba1d24342fc77c7e767a80c666a18fd8a9f7b82419c1c"
|
||||
name = "github.com/PuerkitoBio/urlesc"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "de5bf2ad457846296e2031421a34e2568e304e35"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:60d5ffa86fb76749180841154b3714cde4771723b7e52329fb97ec916a6a68a6"
|
||||
name = "github.com/alecthomas/assert"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "405dbfeb8e38effee6e723317226e93fff912d06"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:2d78d7ffff1988e613e4946d6a6914190db0b38d533e392208797d2f557c92b6"
|
||||
name = "github.com/alecthomas/chroma"
|
||||
packages = [
|
||||
".",
|
||||
"formatters",
|
||||
"formatters/html",
|
||||
"lexers",
|
||||
"lexers/a",
|
||||
"lexers/b",
|
||||
"lexers/c",
|
||||
"lexers/circular",
|
||||
"lexers/d",
|
||||
"lexers/e",
|
||||
"lexers/f",
|
||||
"lexers/g",
|
||||
"lexers/h",
|
||||
"lexers/i",
|
||||
"lexers/internal",
|
||||
"lexers/j",
|
||||
"lexers/k",
|
||||
"lexers/l",
|
||||
"lexers/m",
|
||||
"lexers/n",
|
||||
"lexers/o",
|
||||
"lexers/p",
|
||||
"lexers/q",
|
||||
"lexers/r",
|
||||
"lexers/s",
|
||||
"lexers/t",
|
||||
"lexers/v",
|
||||
"lexers/w",
|
||||
"lexers/x",
|
||||
"lexers/y",
|
||||
"styles",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "1b755a90bd109f170385cb3964f0abdfd3451145"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:ba0a389824c3c3d378f5f334370524b7d04b5a5ccedce7dd2da05ae8d3868609"
|
||||
name = "github.com/alecthomas/colour"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "60882d9e27213e8552dcff6328914fe4c2b44bc9"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:5723237c9114bb92b4571532d9580dd0ed885ec3eb371d58085c074895c04a51"
|
||||
name = "github.com/alecthomas/repr"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "f49988b46e025398b9f834f7c726afe001ec481f"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:05d0e4ae6b8d0273647964fe26762e3345d5d196aaeb858838e75336703451ba"
|
||||
name = "github.com/bep/debounce"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "844797fa1dd9ba969d71b62797ff19d1e49d4eac"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:88b2a2ccfbcf8aa697ec140e196c524f40bbdc8e6f6d03ff79354bc694cb677d"
|
||||
name = "github.com/bep/gitmap"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "012701e8669671499fc43e9792335a1dcbfe2afb"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:7337271448975bc9cf3deae77bb2d7f7fc937c60c59b606ffcd25bfc754dd2ec"
|
||||
name = "github.com/bep/go-tocss"
|
||||
packages = [
|
||||
"scss",
|
||||
"scss/libsass",
|
||||
"tocss",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "2abb118dc8688b6c7df44e12f4152c2bded9b19c"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:e5ad472b763adca2591568384b8f07974d2da4a98b55ffb360e2cecc2a216bae"
|
||||
name = "github.com/chaseadamsio/goorgeous"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "dcf1ef873b8987bf12596fe6951c48347986eb2f"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:f30036d9c6eb2b9262edac69b5f6b59e25b25310ffee3035ab0e7c4b722a8552"
|
||||
name = "github.com/cpuguy83/go-md2man"
|
||||
packages = ["md2man"]
|
||||
pruneopts = ""
|
||||
revision = "a65d4d2de4d5f7c74868dfa9b202a3c8be315aaa"
|
||||
version = "v1.0.6"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:8f6c36ee73ded871996ada894e20dfecde56557d08b4d1849d5489076679971f"
|
||||
name = "github.com/danwakefield/fnmatch"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "cbb64ac3d964b81592e64f957ad53df015803288"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:0a39ec8bf5629610a4bc7873a92039ee509246da3cef1a0ea60f1ed7e5f9cea5"
|
||||
name = "github.com/davecgh/go-spew"
|
||||
packages = ["spew"]
|
||||
pruneopts = ""
|
||||
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:8dc164acd41f84c9f704362d061f90cf45014c3b7eb3fead126efb7d152e2efc"
|
||||
name = "github.com/disintegration/imaging"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "dd50a3ee9985ccd313a2f03c398fcaedc96dc707"
|
||||
version = "v1.2.4"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:536d2bdf1cdef7ec49e1cb4a8a083ae639159f0fd11a5829fe4d35ecb8cc3a1a"
|
||||
name = "github.com/dlclark/regexp2"
|
||||
packages = [
|
||||
".",
|
||||
"syntax",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "487489b64fb796de2e55f4e8a4ad1e145f80e957"
|
||||
version = "v1.1.6"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:fbbae68d476d61afb78178dd4c5532dbfff154e2fea72d1312138bf35c613ec1"
|
||||
name = "github.com/eknkc/amber"
|
||||
packages = [
|
||||
".",
|
||||
"parser",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "cdade1c073850f4ffc70a829e31235ea6892853b"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:e5c807ac3b60699ccec9263f6eec756251b17e78582eb149f90ac345d9e58327"
|
||||
name = "github.com/fortytw2/leaktest"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "a5ef70473c97b71626b9abeda80ee92ba2a7de9e"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:b2106f1668ea5efc1ecc480f7e922a093adb9563fd9ce58585292871f0d0f229"
|
||||
name = "github.com/fsnotify/fsnotify"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9"
|
||||
version = "v1.4.7"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:b7a7e17513aeee6492d93015c7bf29c86a0c1c91210ea56b21e36c1a40958cba"
|
||||
name = "github.com/gobwas/glob"
|
||||
packages = [
|
||||
".",
|
||||
"compiler",
|
||||
"match",
|
||||
"syntax",
|
||||
"syntax/ast",
|
||||
"syntax/lexer",
|
||||
"util/runes",
|
||||
"util/strings",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "5ccd90ef52e1e632236f7326478d4faa74f99438"
|
||||
version = "v0.2.3"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:fe1b4d4cbe48c0d55507c55f8663aa4185576cc58fa0c8be03bb8f19dfe17a9c"
|
||||
name = "github.com/gorilla/websocket"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:4423ee95d6ee30bb22f680445c58889bb5b91e1b955405bf34374a053784a8a2"
|
||||
name = "github.com/hashicorp/go-immutable-radix"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "7f3cd4390caab3250a57f30efdb2a65dd7649ecf"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:9c776d7d9c54b7ed89f119e449983c3f24c0023e75001d6092442412ebca6b94"
|
||||
name = "github.com/hashicorp/golang-lru"
|
||||
packages = ["simplelru"]
|
||||
pruneopts = ""
|
||||
revision = "0fb14efe8c47ae851c0034ed7a448854d3d34cf3"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:ccfc44438ba7fc24effffbe9c7b75bbbb51e7a275e361e62a4dbffbc9e8e43d9"
|
||||
name = "github.com/hashicorp/hcl"
|
||||
packages = [
|
||||
".",
|
||||
"hcl/ast",
|
||||
"hcl/parser",
|
||||
"hcl/printer",
|
||||
"hcl/scanner",
|
||||
"hcl/strconv",
|
||||
"hcl/token",
|
||||
"json/parser",
|
||||
"json/scanner",
|
||||
"json/token",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "ef8a98b0bbce4a65b5aa4c368430a80ddc533168"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:870d441fe217b8e689d7949fef6e43efbc787e50f200cb1e70dbca9204a1d6be"
|
||||
name = "github.com/inconshreveable/mousetrap"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75"
|
||||
version = "v1.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:3b82a4308adc7319d0c7efbad106e966cbbf70e47f16fb5f7aca131b01f6f9b1"
|
||||
name = "github.com/jdkato/prose"
|
||||
packages = [
|
||||
"internal/util",
|
||||
"transform",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "20d3663d4bc9dd10d75abcde9d92e04b4861c674"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:f72872791669f287262777a6a635c7aaebbdd9919cffacea802f8f9c1dc2e6f6"
|
||||
name = "github.com/kyokomi/emoji"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "7e06b236c489543f53868841f188a294e3383eab"
|
||||
version = "v1.5"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:3ee69edc976a8e66debd179073163cbbef426faa7c0ef2d8ff8e3be159699d2e"
|
||||
name = "github.com/magefile/mage"
|
||||
packages = [
|
||||
"mg",
|
||||
"sh",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "2f974307b636f59c13b88704cf350a4772fef271"
|
||||
version = "v1.0.2"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:093cd7ebfdecf5692c66f0b0f7a1877d796c89d9940f5769439c53dc65264036"
|
||||
name = "github.com/magiconair/properties"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "c3beff4c2358b44d0493c7dda585e7db7ff28ae6"
|
||||
version = "v1.7.6"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:fd46423a2fe75d52968baf37471b99e6f05cea3ac29c756128ead26af34e1c35"
|
||||
name = "github.com/markbates/inflect"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "a12c3aec81a6a938bf584a4bac567afed9256586"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:78229b46ddb7434f881390029bd1af7661294af31f6802e0e1bedaad4ab0af3c"
|
||||
name = "github.com/mattn/go-isatty"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
|
||||
version = "v0.0.3"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:81e673df85e765593a863f67cba4544cf40e8919590f04d67664940786c2b61a"
|
||||
name = "github.com/mattn/go-runewidth"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "9e777a8366cce605130a531d2cd6363d07ad7317"
|
||||
version = "v0.0.2"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:6066f4c384723dd468c96adc13bfe4d977f182c2305e1a82c94bb50e1507d528"
|
||||
name = "github.com/miekg/mmark"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "fd2f6c1403b37925bd7fe13af05853b8ae58ee5f"
|
||||
version = "v1.3.6"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:0de0f377aeccd41384e883c59c6f184c9db01c96db33a2724a1eaadd60f92629"
|
||||
name = "github.com/mitchellh/hashstructure"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "2bca23e0e452137f789efbc8610126fd8b94f73b"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:59fa50d593e5673a0dfffa1852b66fd700c05b35e368680b4b89a68fdb2c1379"
|
||||
name = "github.com/mitchellh/mapstructure"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "00c29f56e2386353d58c599509e8dc3801b0d716"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:08b8db6381176e5472815f67f29b2b8b21d5bf3adaba4de3e5416faaf26f3689"
|
||||
name = "github.com/muesli/smartcrop"
|
||||
packages = [
|
||||
".",
|
||||
"options",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "f6ebaa786a12a0fdb2d7c6dee72808e68c296464"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:34afc91b88b56c71639e275ff3ea8179a8f094ddc86df96e26074000cade42fe"
|
||||
name = "github.com/nicksnyder/go-i18n"
|
||||
packages = [
|
||||
"i18n/bundle",
|
||||
"i18n/language",
|
||||
"i18n/translation",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "0dc1626d56435e9d605a29875701721c54bc9bbd"
|
||||
version = "v1.10.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:2656200f82783893859c77903afc794d97c1f5054e2b57f7021e33a6047e7b1e"
|
||||
name = "github.com/olekukonko/tablewriter"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "b8a9be070da40449e501c3c4730a889e42d87a9e"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:fef37519c971ab3c464318a0c9da6a7fb9c21439aa587e61bf10af651e6119b9"
|
||||
name = "github.com/pelletier/go-toml"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "acdc4509485b587f5e675510c4f2c63e90ff68a8"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:256484dbbcd271f9ecebc6795b2df8cad4c458dd0f5fd82a8c2fa0c29f233411"
|
||||
name = "github.com/pmezard/go-difflib"
|
||||
packages = ["difflib"]
|
||||
pruneopts = ""
|
||||
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:10bc894f31df5ca3366a50122ee2ade6b56f3f5531c523a8a551748a0663aa1b"
|
||||
name = "github.com/russross/blackfriday"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "11635eb403ff09dbc3a6b5a007ab5ab09151c229"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:afa9b7bb3d9e633ef32e46ab4b6a87b5376271e3d86b09a379029bbb10e26fa0"
|
||||
name = "github.com/sanity-io/litter"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "ae543b7ba8fd6af63e4976198f146e1348ae53c1"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:1ebe873a8dc99e3316c30fea2e211038c4d1fcb605eee17e00a5e91b8817925e"
|
||||
name = "github.com/sergi/go-diff"
|
||||
packages = ["diffmatchpatch"]
|
||||
pruneopts = ""
|
||||
revision = "1744e2970ca51c86172c8190fadad617561ed6e7"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:606fa779c7a80ab6a9ec5836cd12759f86cd6e1d82d5d60cf7ec5133e78c6cf8"
|
||||
name = "github.com/shurcooL/sanitized_anchor_name"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "86672fcb3f950f35f2e675df2240550f2a50762f"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:3d6c88f060fdaac3291f3d18f58eb542fe3a2b330609b269962d26d0c99104be"
|
||||
name = "github.com/spf13/afero"
|
||||
packages = [
|
||||
".",
|
||||
"mem",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "787d034dfe70e44075ccc060d346146ef53270ad"
|
||||
version = "v1.1.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:d0b38ba6da419a6d4380700218eeec8623841d44a856bb57369c172fbf692ab4"
|
||||
name = "github.com/spf13/cast"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "8965335b8c7107321228e3e3702cab9832751bac"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:9e14345bfb9aa30952b41ffceaa2acd559933cfe3d17bd87b7f6e54b6c618492"
|
||||
name = "github.com/spf13/cobra"
|
||||
packages = [
|
||||
".",
|
||||
"doc",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "a1f051bc3eba734da4772d60e2d677f47cf93ef4"
|
||||
version = "v0.0.2"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:7c00f445c89f468abd5ab86546ba1fa11bac66e63325ade8ca05aa7220791fc7"
|
||||
name = "github.com/spf13/fsync"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "12a01e648f05a938100a26858d2d59a120307a18"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:104517520aab91164020ab6524a5d6b7cafc641b2e42ac6236f6ac1deac4f66a"
|
||||
name = "github.com/spf13/jwalterweatherman"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "7c0cea34c8ece3fbeb2b27ab9b59511d360fb394"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:5082c0d57a199dabc0868010784039249af0932fad2990b51ad9404c9a6b6e5d"
|
||||
name = "github.com/spf13/nitro"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "24d7ef30a12da0bdc5e2eb370a79c659ddccf0e8"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:261bc565833ef4f02121450d74eb88d5ae4bd74bfe5d0e862cddb8550ec35000"
|
||||
name = "github.com/spf13/pflag"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "e57e3eeb33f795204c1ca35f56c44f83227c6e66"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:79af66c513727a0ed32fb4137ad7d0613b01e8e2f5ce9479724e26e2c9068011"
|
||||
name = "github.com/spf13/viper"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "b5e8006cbee93ec955a89ab31e0e3ce3204f3736"
|
||||
version = "v1.0.2"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:a70d585d45f695f2e8e6782569bdf181419667a35e6035ceb086706b495aa21a"
|
||||
name = "github.com/stretchr/testify"
|
||||
packages = [
|
||||
"assert",
|
||||
"require",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71"
|
||||
version = "v1.2.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:54b1c3f97e0d74b571055cc17af4a978b6160e6ccfda73608aba048e404319c1"
|
||||
name = "github.com/tdewolff/minify"
|
||||
packages = [
|
||||
".",
|
||||
"css",
|
||||
"html",
|
||||
"js",
|
||||
"json",
|
||||
"svg",
|
||||
"xml",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "8d72a4127ae33b755e95bffede9b92e396267ce2"
|
||||
version = "v2.3.5"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:33d327ae34260328b00ed15f89961446d805797a96de8643a983020825bb51ae"
|
||||
name = "github.com/tdewolff/parse"
|
||||
packages = [
|
||||
".",
|
||||
"buffer",
|
||||
"css",
|
||||
"html",
|
||||
"js",
|
||||
"json",
|
||||
"strconv",
|
||||
"svg",
|
||||
"xml",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "d739d6fccb0971177e06352fea02d3552625efb1"
|
||||
version = "v2.3.3"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:12e69a2969bdcdf2928607e51966a868482c2a450f1c97c5dde004257c8369ff"
|
||||
name = "github.com/wellington/go-libsass"
|
||||
packages = ["libs"]
|
||||
pruneopts = ""
|
||||
revision = "615eaa47ef794d037c1906a0eb7bf85375a5decf"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:b1e5dc66cd8ad9712eb8fd4dfaecac6ae45cbcde94476c5b64d905c5d789eef7"
|
||||
name = "github.com/yosssi/ace"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "ea038f4770b6746c3f8f84f14fa60d9fe1205b56"
|
||||
version = "v0.0.5"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:4525c51a65e4974007814da8e2621343840d769d80b2b2ab79e6958342726587"
|
||||
name = "golang.org/x/image"
|
||||
packages = [
|
||||
"bmp",
|
||||
"draw",
|
||||
"math/f64",
|
||||
"riff",
|
||||
"tiff",
|
||||
"tiff/lzw",
|
||||
"vp8",
|
||||
"vp8l",
|
||||
"webp",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "f315e440302883054d0c2bd85486878cb4f8572c"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:8bb19e8d6e1321626d3f58ffee75699e86940c79d959b30fcf632883d7c4d303"
|
||||
name = "golang.org/x/net"
|
||||
packages = [
|
||||
"context",
|
||||
"idna",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "61147c48b25b599e5b561d2e9c4f3e1ef489ca41"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:d84d0f563cc649de4c9a8272a0395f75b11952202d18d4d927e933cc91493062"
|
||||
name = "golang.org/x/sync"
|
||||
packages = ["errgroup"]
|
||||
pruneopts = ""
|
||||
revision = "1d60e4601c6fd243af51cc01ddf169918a5407ca"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:137a530894d869c2fb09e135e6bb7a5d701347ba273471eaa2483053f8fbeb35"
|
||||
name = "golang.org/x/sys"
|
||||
packages = ["unix"]
|
||||
pruneopts = ""
|
||||
revision = "3b87a42e500a6dc65dae1a55d0b641295971163e"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:4cf01dae652ef4428cf9d354bdec79467e436ecfa92d943b1ada8184e1a938b6"
|
||||
name = "golang.org/x/text"
|
||||
packages = [
|
||||
"collate",
|
||||
"collate/build",
|
||||
"internal/colltab",
|
||||
"internal/gen",
|
||||
"internal/language",
|
||||
"internal/language/compact",
|
||||
"internal/tag",
|
||||
"internal/triegen",
|
||||
"internal/ucd",
|
||||
"language",
|
||||
"secure/bidirule",
|
||||
"transform",
|
||||
"unicode/bidi",
|
||||
"unicode/cldr",
|
||||
"unicode/norm",
|
||||
"unicode/rangetable",
|
||||
"width",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "2cb43934f0eece38629746959acc633cba083fe4"
|
||||
|
||||
[[projects]]
|
||||
branch = "v2"
|
||||
digest = "1:f0620375dd1f6251d9973b5f2596228cc8042e887cd7f827e4220bc1ce8c30e2"
|
||||
name = "gopkg.in/yaml.v2"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
input-imports = [
|
||||
"github.com/BurntSushi/locker",
|
||||
"github.com/BurntSushi/toml",
|
||||
"github.com/PuerkitoBio/purell",
|
||||
"github.com/alecthomas/assert",
|
||||
"github.com/alecthomas/chroma",
|
||||
"github.com/alecthomas/chroma/formatters",
|
||||
"github.com/alecthomas/chroma/formatters/html",
|
||||
"github.com/alecthomas/chroma/lexers",
|
||||
"github.com/alecthomas/chroma/styles",
|
||||
"github.com/bep/debounce",
|
||||
"github.com/bep/gitmap",
|
||||
"github.com/bep/go-tocss/scss",
|
||||
"github.com/bep/go-tocss/scss/libsass",
|
||||
"github.com/bep/go-tocss/tocss",
|
||||
"github.com/chaseadamsio/goorgeous",
|
||||
"github.com/disintegration/imaging",
|
||||
"github.com/eknkc/amber",
|
||||
"github.com/fortytw2/leaktest",
|
||||
"github.com/fsnotify/fsnotify",
|
||||
"github.com/gobwas/glob",
|
||||
"github.com/gorilla/websocket",
|
||||
"github.com/hashicorp/go-immutable-radix",
|
||||
"github.com/jdkato/prose/transform",
|
||||
"github.com/kyokomi/emoji",
|
||||
"github.com/magefile/mage/mg",
|
||||
"github.com/magefile/mage/sh",
|
||||
"github.com/markbates/inflect",
|
||||
"github.com/miekg/mmark",
|
||||
"github.com/mitchellh/hashstructure",
|
||||
"github.com/mitchellh/mapstructure",
|
||||
"github.com/muesli/smartcrop",
|
||||
"github.com/nicksnyder/go-i18n/i18n/bundle",
|
||||
"github.com/nicksnyder/go-i18n/i18n/language",
|
||||
"github.com/olekukonko/tablewriter",
|
||||
"github.com/russross/blackfriday",
|
||||
"github.com/sanity-io/litter",
|
||||
"github.com/spf13/afero",
|
||||
"github.com/spf13/cast",
|
||||
"github.com/spf13/cobra",
|
||||
"github.com/spf13/cobra/doc",
|
||||
"github.com/spf13/fsync",
|
||||
"github.com/spf13/jwalterweatherman",
|
||||
"github.com/spf13/nitro",
|
||||
"github.com/spf13/pflag",
|
||||
"github.com/spf13/viper",
|
||||
"github.com/stretchr/testify/assert",
|
||||
"github.com/stretchr/testify/require",
|
||||
"github.com/tdewolff/minify",
|
||||
"github.com/tdewolff/minify/css",
|
||||
"github.com/tdewolff/minify/html",
|
||||
"github.com/tdewolff/minify/js",
|
||||
"github.com/tdewolff/minify/json",
|
||||
"github.com/tdewolff/minify/svg",
|
||||
"github.com/tdewolff/minify/xml",
|
||||
"github.com/yosssi/ace",
|
||||
"golang.org/x/image/webp",
|
||||
"golang.org/x/net/context",
|
||||
"golang.org/x/sync/errgroup",
|
||||
"golang.org/x/text/transform",
|
||||
"golang.org/x/text/unicode/norm",
|
||||
"gopkg.in/yaml.v2",
|
||||
]
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
-171
@@ -1,171 +0,0 @@
|
||||
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/BurntSushi/toml"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/PuerkitoBio/purell"
|
||||
version = "1.1.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/alecthomas/chroma"
|
||||
revision = "1b755a90bd109f170385cb3964f0abdfd3451145"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/bep/gitmap"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/bep/go-tocss"
|
||||
|
||||
[[override]]
|
||||
branch = "master"
|
||||
name = "github.com/wellington/go-libsass"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/chaseadamsio/goorgeous"
|
||||
version = "^1.1.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/disintegration/imaging"
|
||||
version = "~v1.2.4"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/magefile/mage"
|
||||
version = "v1"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/eknkc/amber"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/fortytw2/leaktest"
|
||||
version = "1.1.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/fsnotify/fsnotify"
|
||||
version = "^1.4.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/gorilla/websocket"
|
||||
version = "1.2.0"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/hashicorp/go-immutable-radix"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/jdkato/prose"
|
||||
version = "1.1.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/kyokomi/emoji"
|
||||
version = "1.5.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/markbates/inflect"
|
||||
revision = "a12c3aec81a6a938bf584a4bac567afed9256586"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/miekg/mmark"
|
||||
version = "^1.3.6"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/mitchellh/mapstructure"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/nicksnyder/go-i18n"
|
||||
version = "^1.10.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/russross/blackfriday"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/spf13/afero"
|
||||
version = "^1.1.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/spf13/cast"
|
||||
version = "^1.1.0"
|
||||
|
||||
[[constraint]]
|
||||
version = "^0.0.1"
|
||||
name = "github.com/spf13/cobra"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/spf13/fsync"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/spf13/jwalterweatherman"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/spf13/nitro"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/spf13/pflag"
|
||||
version = "1.0.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/spf13/viper"
|
||||
version = "1.0.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/stretchr/testify"
|
||||
version = "1.1.4"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/olekukonko/tablewriter"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/yosssi/ace"
|
||||
version = "^0.0.5"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/image"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/text"
|
||||
|
||||
[[constraint]]
|
||||
branch = "v2"
|
||||
name = "gopkg.in/yaml.v2"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/gobwas/glob"
|
||||
version = "0.2.2"
|
||||
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/muesli/smartcrop"
|
||||
branch = "master"
|
||||
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/sanity-io/litter"
|
||||
version = "1.1.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/bep/debounce"
|
||||
version = "^1.1.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/tdewolff/minify"
|
||||
version = "^2.3.5"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/BurntSushi/locker"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/mitchellh/hashstructure"
|
||||
@@ -4,16 +4,13 @@ A Fast and Flexible Static Site Generator built with love by [bep](https://githu
|
||||
|
||||
[Website](https://gohugo.io) |
|
||||
[Forum](https://discourse.gohugo.io) |
|
||||
[Developer Chat (no support)](https://gitter.im/gohugoio/hugo) |
|
||||
[Documentation](https://gohugo.io/overview/introduction/) |
|
||||
[Installation Guide](https://gohugo.io/overview/installing/) |
|
||||
[Documentation](https://gohugo.io/getting-started/) |
|
||||
[Installation Guide](https://gohugo.io/getting-started/installing/) |
|
||||
[Contribution Guide](CONTRIBUTING.md) |
|
||||
[Twitter](http://twitter.com/gohugoio)
|
||||
[Twitter](https://twitter.com/gohugoio)
|
||||
|
||||
[](https://godoc.org/github.com/gohugoio/hugo)
|
||||
[](https://travis-ci.org/gohugoio/hugo)
|
||||
[](https://ci.appveyor.com/project/bep/hugo/branch/master)
|
||||
[](https://gitter.im/spf13/hugo?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
[](https://travis-ci.org/gohugoio/hugo)
|
||||
[](https://goreportcard.com/report/github.com/gohugoio/hugo)
|
||||
|
||||
## Overview
|
||||
@@ -36,7 +33,7 @@ Currently, we provide pre-built Hugo binaries for Windows, Linux, FreeBSD, NetBS
|
||||
|
||||
Hugo may also be compiled from source wherever the Go compiler tool chain can run, e.g. for other operating systems including DragonFly BSD, OpenBSD, Plan 9, and Solaris.
|
||||
|
||||
**Complete documentation is available at [Hugo Documentation][].**
|
||||
**Complete documentation is available at [Hugo Documentation](https://gohugo.io/getting-started/).**
|
||||
|
||||
## Choose How to Install
|
||||
|
||||
@@ -50,29 +47,25 @@ Building the binaries is an easy task for an experienced `go` getter.
|
||||
|
||||
### Install Hugo as Your Site Generator (Binary Install)
|
||||
|
||||
Use the [installation instructions in the Hugo documentation](https://gohugo.io/overview/installing/).
|
||||
Use the [installation instructions in the Hugo documentation](https://gohugo.io/getting-started/installing/).
|
||||
|
||||
### Build and Install the Binaries from Source (Advanced Install)
|
||||
|
||||
#### Prerequisite Tools
|
||||
|
||||
* [Git](http://git-scm.com/)
|
||||
* [Go (latest or previous version)](https://golang.org/dl/)
|
||||
|
||||
#### Vendored Dependencies
|
||||
|
||||
Hugo uses [dep](https://github.com/golang/dep) to vendor dependencies, but we don't commit the vendored packages themselves to the Hugo git repository. Therefore, a simple `go get` is _not_ supported because the command is not vendor aware.
|
||||
|
||||
The simplest way is to use [mage](https://github.com/magefile/mage) (a Make alternative for Go projects.)
|
||||
* [Git](https://git-scm.com/)
|
||||
* [Go (at least Go 1.11)](https://golang.org/dl/)
|
||||
|
||||
#### Fetch from GitHub
|
||||
|
||||
Since Hugo 0.48, Hugo uses the Go Modules support built into Go 1.11 to build. The easiest is to clone Hugo in a directory outside of `GOPATH`, as in the following example:
|
||||
|
||||
```bash
|
||||
go get github.com/magefile/mage
|
||||
go get -d github.com/gohugoio/hugo
|
||||
cd ${GOPATH:-$HOME/go}/src/github.com/gohugoio/hugo
|
||||
mage vendor
|
||||
mage install
|
||||
mkdir $HOME/src
|
||||
cd $HOME/src
|
||||
git clone https://github.com/gohugoio/hugo.git
|
||||
cd hugo
|
||||
go install
|
||||
```
|
||||
|
||||
**If you are a Windows user, substitute the `$HOME` environment variable above with `%USERPROFILE%`.**
|
||||
@@ -124,3 +117,71 @@ For a complete guide to contributing code to Hugo, see the [Contribution Guide](
|
||||
|
||||
[Go]: https://golang.org/
|
||||
[Hugo Documentation]: https://gohugo.io/overview/introduction/
|
||||
|
||||
## Dependencies
|
||||
|
||||
Hugo stands on the shoulder of many great open source libraries, in lexical order:
|
||||
|
||||
| Dependency | 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/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/alecthomas/chroma](https://github.com/alecthomas/chroma) | MIT License |
|
||||
| [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/go-tocss](https://github.com/bep/go-tocss) | MIT License |
|
||||
| [github.com/niklasfasching/go-org](https://github.com/niklasfasching/go-org) | 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/imaging](https://github.com/disintegration/imaging) | MIT License |
|
||||
| [github.com/dlclark/regexp2](https://github.com/dlclark/regexp2) | MIT License |
|
||||
| [github.com/eknkc/amber](https://github.com/eknkc/amber) | 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/go-immutable-radix](https://github.com/hashicorp/go-immutable-radix) | Mozilla Public License 2.0 |
|
||||
| [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/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/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/russross/blackfriday](https://github.com/russross/blackfriday) | Simplified BSD License |
|
||||
| [github.com/shurcooL/sanitized_anchor_name](https://github.com/shurcooL/sanitized_anchor_name) | MIT 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/nitro](https://github.com/spf13/nitro) | Apache License 2.0 |
|
||||
| [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/wellington/go-libsass](https://github.com/wellington/go-libsass) | Apache License 2.0 |
|
||||
| [github.com/yosssi/ace](https://github.com/yosssi/ace) | MIT License |
|
||||
| [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/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
|
||||
| [gopkg.in/yaml.v2](https://gopkg.in/yaml.v2) | Apache License 2.0 |
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
image: Visual Studio 2015
|
||||
|
||||
init:
|
||||
- set PATH=%PATH%;C:\mingw-w64\x86_64-7.3.0-posix-seh-rt_v5-rev0\mingw64\bin;%GOPATH%\bin
|
||||
- go version
|
||||
- go env
|
||||
|
||||
environment:
|
||||
GOPATH: C:\GOPATH\
|
||||
HUGO_BUILD_TAGS: extended
|
||||
|
||||
# clones and cd's to path
|
||||
clone_folder: C:\GOPATH\src\github.com\gohugoio\hugo
|
||||
|
||||
install:
|
||||
# - gem install asciidoctor
|
||||
- pip install docutils
|
||||
- go get github.com/magefile/mage
|
||||
|
||||
build_script:
|
||||
- mage vendor hugoRace
|
||||
- mage -v check
|
||||
- hugo -s docs/
|
||||
- hugo --renderToMemory -s docs/
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
gobench -package=./hugolib -bench="BenchmarkSiteNew/Deep_content_tree"
|
||||
+1
-2
@@ -1,2 +1 @@
|
||||
# Temp script used to test new builds.
|
||||
docker run --rm --mount type=bind,source="$(pwd)",target=/go/src/github.com/gohugoio/hugo -w /go/src/github.com/gohugoio/hugo -i -t bepsays/ci-goreleaser:latest /bin/bash
|
||||
docker run --rm --mount type=bind,source="$(pwd)",target=/hugo -w /hugo -i -t bepsays/ci-goreleaser:1.11-2 /bin/bash
|
||||
Vendored
+358
@@ -0,0 +1,358 @@
|
||||
// Copyright 2018 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 filecache
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/common/hugio"
|
||||
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
|
||||
"github.com/BurntSushi/locker"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
const (
|
||||
filecacheRootDirname = "filecache"
|
||||
)
|
||||
|
||||
// Cache caches a set of files in a directory. This is usually a file on
|
||||
// disk, but since this is backed by an Afero file system, it can be anything.
|
||||
type Cache struct {
|
||||
Fs afero.Fs
|
||||
|
||||
// Max age for items in this cache. Negative duration means forever,
|
||||
// 0 is effectively turning this cache off.
|
||||
maxAge time.Duration
|
||||
|
||||
// When set, we just remove this entire root directory on expiration.
|
||||
pruneAllRootDir string
|
||||
|
||||
nlocker *lockTracker
|
||||
}
|
||||
|
||||
type lockTracker struct {
|
||||
seenMu sync.RWMutex
|
||||
seen map[string]struct{}
|
||||
|
||||
*locker.Locker
|
||||
}
|
||||
|
||||
// Lock tracks the ids in use. We use this information to do garbage collection
|
||||
// after a Hugo build.
|
||||
func (l *lockTracker) Lock(id string) {
|
||||
l.seenMu.RLock()
|
||||
if _, seen := l.seen[id]; !seen {
|
||||
l.seenMu.RUnlock()
|
||||
l.seenMu.Lock()
|
||||
l.seen[id] = struct{}{}
|
||||
l.seenMu.Unlock()
|
||||
} else {
|
||||
l.seenMu.RUnlock()
|
||||
}
|
||||
|
||||
l.Locker.Lock(id)
|
||||
}
|
||||
|
||||
// ItemInfo contains info about a cached file.
|
||||
type ItemInfo struct {
|
||||
// This is the file's name relative to the cache's filesystem.
|
||||
Name string
|
||||
}
|
||||
|
||||
// NewCache creates a new file cache with the given filesystem and max age.
|
||||
func NewCache(fs afero.Fs, maxAge time.Duration, pruneAllRootDir string) *Cache {
|
||||
return &Cache{
|
||||
Fs: fs,
|
||||
nlocker: &lockTracker{Locker: locker.NewLocker(), seen: make(map[string]struct{})},
|
||||
maxAge: maxAge,
|
||||
pruneAllRootDir: pruneAllRootDir,
|
||||
}
|
||||
}
|
||||
|
||||
// lockedFile is a file with a lock that is released on Close.
|
||||
type lockedFile struct {
|
||||
afero.File
|
||||
unlock func()
|
||||
}
|
||||
|
||||
func (l *lockedFile) Close() error {
|
||||
defer l.unlock()
|
||||
return l.File.Close()
|
||||
}
|
||||
|
||||
// WriteCloser returns a transactional writer into the cache.
|
||||
// It's important that it's closed when done.
|
||||
func (c *Cache) WriteCloser(id string) (ItemInfo, io.WriteCloser, error) {
|
||||
id = cleanID(id)
|
||||
c.nlocker.Lock(id)
|
||||
|
||||
info := ItemInfo{Name: id}
|
||||
|
||||
f, err := helpers.OpenFileForWriting(c.Fs, id)
|
||||
if err != nil {
|
||||
c.nlocker.Unlock(id)
|
||||
return info, nil, err
|
||||
}
|
||||
|
||||
return info, &lockedFile{
|
||||
File: f,
|
||||
unlock: func() { c.nlocker.Unlock(id) },
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ReadOrCreate tries to lookup the file in cache.
|
||||
// If found, it is passed to read and then closed.
|
||||
// If not found a new file is created and passed to create, which should close
|
||||
// it when done.
|
||||
func (c *Cache) ReadOrCreate(id string,
|
||||
read func(info ItemInfo, r io.Reader) error,
|
||||
create func(info ItemInfo, w io.WriteCloser) error) (info ItemInfo, err error) {
|
||||
id = cleanID(id)
|
||||
|
||||
c.nlocker.Lock(id)
|
||||
defer c.nlocker.Unlock(id)
|
||||
|
||||
info = ItemInfo{Name: id}
|
||||
|
||||
if r := c.getOrRemove(id); r != nil {
|
||||
err = read(info, r)
|
||||
defer r.Close()
|
||||
return
|
||||
}
|
||||
|
||||
f, err := helpers.OpenFileForWriting(c.Fs, id)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = create(info, f)
|
||||
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
// GetOrCreate tries to get the file with the given id from cache. If not found or expired, create will
|
||||
// be invoked and the result cached.
|
||||
// This method is protected by a named lock using the given id as identifier.
|
||||
func (c *Cache) GetOrCreate(id string, create func() (io.ReadCloser, error)) (ItemInfo, io.ReadCloser, error) {
|
||||
id = cleanID(id)
|
||||
|
||||
c.nlocker.Lock(id)
|
||||
defer c.nlocker.Unlock(id)
|
||||
|
||||
info := ItemInfo{Name: id}
|
||||
|
||||
if r := c.getOrRemove(id); r != nil {
|
||||
return info, r, nil
|
||||
}
|
||||
|
||||
r, err := create()
|
||||
if err != nil {
|
||||
return info, nil, err
|
||||
}
|
||||
|
||||
if c.maxAge == 0 {
|
||||
// No caching.
|
||||
return info, hugio.ToReadCloser(r), nil
|
||||
}
|
||||
|
||||
var buff bytes.Buffer
|
||||
return info,
|
||||
hugio.ToReadCloser(&buff),
|
||||
afero.WriteReader(c.Fs, id, io.TeeReader(r, &buff))
|
||||
}
|
||||
|
||||
// GetOrCreateBytes is the same as GetOrCreate, but produces a byte slice.
|
||||
func (c *Cache) GetOrCreateBytes(id string, create func() ([]byte, error)) (ItemInfo, []byte, error) {
|
||||
id = cleanID(id)
|
||||
|
||||
c.nlocker.Lock(id)
|
||||
defer c.nlocker.Unlock(id)
|
||||
|
||||
info := ItemInfo{Name: id}
|
||||
|
||||
if r := c.getOrRemove(id); r != nil {
|
||||
defer r.Close()
|
||||
b, err := ioutil.ReadAll(r)
|
||||
return info, b, err
|
||||
}
|
||||
|
||||
b, err := create()
|
||||
if err != nil {
|
||||
return info, nil, err
|
||||
}
|
||||
|
||||
if c.maxAge == 0 {
|
||||
return info, b, nil
|
||||
}
|
||||
|
||||
if err := afero.WriteReader(c.Fs, id, bytes.NewReader(b)); err != nil {
|
||||
return info, nil, err
|
||||
}
|
||||
return info, b, nil
|
||||
|
||||
}
|
||||
|
||||
// GetBytes gets the file content with the given id from the cahce, nil if none found.
|
||||
func (c *Cache) GetBytes(id string) (ItemInfo, []byte, error) {
|
||||
id = cleanID(id)
|
||||
|
||||
c.nlocker.Lock(id)
|
||||
defer c.nlocker.Unlock(id)
|
||||
|
||||
info := ItemInfo{Name: id}
|
||||
|
||||
if r := c.getOrRemove(id); r != nil {
|
||||
defer r.Close()
|
||||
b, err := ioutil.ReadAll(r)
|
||||
return info, b, err
|
||||
}
|
||||
|
||||
return info, nil, nil
|
||||
}
|
||||
|
||||
// Get gets the file with the given id from the cahce, nil if none found.
|
||||
func (c *Cache) Get(id string) (ItemInfo, io.ReadCloser, error) {
|
||||
id = cleanID(id)
|
||||
|
||||
c.nlocker.Lock(id)
|
||||
defer c.nlocker.Unlock(id)
|
||||
|
||||
info := ItemInfo{Name: id}
|
||||
|
||||
r := c.getOrRemove(id)
|
||||
|
||||
return info, r, nil
|
||||
}
|
||||
|
||||
// getOrRemove gets the file with the given id. If it's expired, it will
|
||||
// be removed.
|
||||
func (c *Cache) getOrRemove(id string) hugio.ReadSeekCloser {
|
||||
if c.maxAge == 0 {
|
||||
// No caching.
|
||||
return nil
|
||||
}
|
||||
|
||||
if c.maxAge > 0 {
|
||||
fi, err := c.Fs.Stat(id)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if c.isExpired(fi.ModTime()) {
|
||||
c.Fs.Remove(id)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
f, err := c.Fs.Open(id)
|
||||
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
func (c *Cache) isExpired(modTime time.Time) bool {
|
||||
if c.maxAge < 0 {
|
||||
return false
|
||||
}
|
||||
return c.maxAge == 0 || time.Since(modTime) > c.maxAge
|
||||
}
|
||||
|
||||
// For testing
|
||||
func (c *Cache) getString(id string) string {
|
||||
id = cleanID(id)
|
||||
|
||||
c.nlocker.Lock(id)
|
||||
defer c.nlocker.Unlock(id)
|
||||
|
||||
f, err := c.Fs.Open(id)
|
||||
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
b, _ := ioutil.ReadAll(f)
|
||||
return string(b)
|
||||
|
||||
}
|
||||
|
||||
// Caches is a named set of caches.
|
||||
type Caches map[string]*Cache
|
||||
|
||||
// Get gets a named cache, nil if none found.
|
||||
func (f Caches) Get(name string) *Cache {
|
||||
return f[strings.ToLower(name)]
|
||||
}
|
||||
|
||||
// NewCaches creates a new set of file caches from the given
|
||||
// configuration.
|
||||
func NewCaches(p *helpers.PathSpec) (Caches, error) {
|
||||
var dcfg Configs
|
||||
if c, ok := p.Cfg.Get("filecacheConfigs").(Configs); ok {
|
||||
dcfg = c
|
||||
} else {
|
||||
var err error
|
||||
dcfg, err = DecodeConfig(p.Fs.Source, p.Cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
fs := p.Fs.Source
|
||||
|
||||
m := make(Caches)
|
||||
for k, v := range dcfg {
|
||||
var cfs afero.Fs
|
||||
|
||||
if v.isResourceDir {
|
||||
cfs = p.BaseFs.ResourcesCache
|
||||
} else {
|
||||
cfs = fs
|
||||
}
|
||||
|
||||
baseDir := v.Dir
|
||||
|
||||
if err := cfs.MkdirAll(baseDir, 0777); err != nil && !os.IsExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bfs := afero.NewBasePathFs(cfs, baseDir)
|
||||
|
||||
var pruneAllRootDir string
|
||||
if k == cacheKeyModules {
|
||||
pruneAllRootDir = "pkg"
|
||||
}
|
||||
|
||||
m[k] = NewCache(bfs, v.MaxAge, pruneAllRootDir)
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func cleanID(name string) string {
|
||||
return strings.TrimPrefix(filepath.Clean(name), helpers.FilePathSeparator)
|
||||
}
|
||||
Vendored
+230
@@ -0,0 +1,230 @@
|
||||
// Copyright 2018 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 filecache
|
||||
|
||||
import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/config"
|
||||
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
const (
|
||||
cachesConfigKey = "caches"
|
||||
|
||||
resourcesGenDir = ":resourceDir/_gen"
|
||||
)
|
||||
|
||||
var defaultCacheConfig = Config{
|
||||
MaxAge: -1, // Never expire
|
||||
Dir: ":cacheDir/:project",
|
||||
}
|
||||
|
||||
const (
|
||||
cacheKeyGetJSON = "getjson"
|
||||
cacheKeyGetCSV = "getcsv"
|
||||
cacheKeyImages = "images"
|
||||
cacheKeyAssets = "assets"
|
||||
cacheKeyModules = "modules"
|
||||
)
|
||||
|
||||
type Configs map[string]Config
|
||||
|
||||
func (c Configs) CacheDirModules() string {
|
||||
return c[cacheKeyModules].Dir
|
||||
}
|
||||
|
||||
var defaultCacheConfigs = Configs{
|
||||
cacheKeyModules: {
|
||||
MaxAge: -1,
|
||||
Dir: ":cacheDir/modules",
|
||||
},
|
||||
cacheKeyGetJSON: defaultCacheConfig,
|
||||
cacheKeyGetCSV: defaultCacheConfig,
|
||||
cacheKeyImages: {
|
||||
MaxAge: -1,
|
||||
Dir: resourcesGenDir,
|
||||
},
|
||||
cacheKeyAssets: {
|
||||
MaxAge: -1,
|
||||
Dir: resourcesGenDir,
|
||||
},
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
// Max age of cache entries in this cache. Any items older than this will
|
||||
// be removed and not returned from the cache.
|
||||
// a negative value means forever, 0 means cache is disabled.
|
||||
MaxAge time.Duration
|
||||
|
||||
// The directory where files are stored.
|
||||
Dir string
|
||||
|
||||
// Will resources/_gen will get its own composite filesystem that
|
||||
// also checks any theme.
|
||||
isResourceDir bool
|
||||
}
|
||||
|
||||
// GetJSONCache gets the file cache for getJSON.
|
||||
func (f Caches) GetJSONCache() *Cache {
|
||||
return f[cacheKeyGetJSON]
|
||||
}
|
||||
|
||||
// GetCSVCache gets the file cache for getCSV.
|
||||
func (f Caches) GetCSVCache() *Cache {
|
||||
return f[cacheKeyGetCSV]
|
||||
}
|
||||
|
||||
// ImageCache gets the file cache for processed images.
|
||||
func (f Caches) ImageCache() *Cache {
|
||||
return f[cacheKeyImages]
|
||||
}
|
||||
|
||||
// ModulesCache gets the file cache for Hugo Modules.
|
||||
func (f Caches) ModulesCache() *Cache {
|
||||
return f[cacheKeyModules]
|
||||
}
|
||||
|
||||
// AssetsCache gets the file cache for assets (processed resources, SCSS etc.).
|
||||
func (f Caches) AssetsCache() *Cache {
|
||||
return f[cacheKeyAssets]
|
||||
}
|
||||
|
||||
func DecodeConfig(fs afero.Fs, cfg config.Provider) (Configs, error) {
|
||||
c := make(Configs)
|
||||
valid := make(map[string]bool)
|
||||
// Add defaults
|
||||
for k, v := range defaultCacheConfigs {
|
||||
c[k] = v
|
||||
valid[k] = true
|
||||
}
|
||||
|
||||
m := cfg.GetStringMap(cachesConfigKey)
|
||||
|
||||
_, isOsFs := fs.(*afero.OsFs)
|
||||
|
||||
for k, v := range m {
|
||||
cc := defaultCacheConfig
|
||||
|
||||
dc := &mapstructure.DecoderConfig{
|
||||
Result: &cc,
|
||||
DecodeHook: mapstructure.StringToTimeDurationHookFunc(),
|
||||
WeaklyTypedInput: true,
|
||||
}
|
||||
|
||||
decoder, err := mapstructure.NewDecoder(dc)
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
|
||||
if err := decoder.Decode(v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cc.Dir == "" {
|
||||
return c, errors.New("must provide cache Dir")
|
||||
}
|
||||
|
||||
name := strings.ToLower(k)
|
||||
if !valid[name] {
|
||||
return nil, errors.Errorf("%q is not a valid cache name", name)
|
||||
}
|
||||
|
||||
c[name] = cc
|
||||
}
|
||||
|
||||
// This is a very old flag in Hugo, but we need to respect it.
|
||||
disabled := cfg.GetBool("ignoreCache")
|
||||
|
||||
for k, v := range c {
|
||||
dir := filepath.ToSlash(filepath.Clean(v.Dir))
|
||||
hadSlash := strings.HasPrefix(dir, "/")
|
||||
parts := strings.Split(dir, "/")
|
||||
|
||||
for i, part := range parts {
|
||||
if strings.HasPrefix(part, ":") {
|
||||
resolved, isResource, err := resolveDirPlaceholder(fs, cfg, part)
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
if isResource {
|
||||
v.isResourceDir = true
|
||||
}
|
||||
parts[i] = resolved
|
||||
}
|
||||
}
|
||||
|
||||
dir = path.Join(parts...)
|
||||
if hadSlash {
|
||||
dir = "/" + dir
|
||||
}
|
||||
v.Dir = filepath.Clean(filepath.FromSlash(dir))
|
||||
|
||||
if !v.isResourceDir {
|
||||
if isOsFs && !filepath.IsAbs(v.Dir) {
|
||||
return c, errors.Errorf("%q must resolve to an absolute directory", v.Dir)
|
||||
}
|
||||
|
||||
// Avoid cache in root, e.g. / (Unix) or c:\ (Windows)
|
||||
if len(strings.TrimPrefix(v.Dir, filepath.VolumeName(v.Dir))) == 1 {
|
||||
return c, errors.Errorf("%q is a root folder and not allowed as cache dir", v.Dir)
|
||||
}
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(v.Dir, "_gen") {
|
||||
// We do cache eviction (file removes) and since the user can set
|
||||
// his/hers own cache directory, we really want to make sure
|
||||
// we do not delete any files that do not belong to this cache.
|
||||
// We do add the cache name as the root, but this is an extra safe
|
||||
// guard. We skip the files inside /resources/_gen/ because
|
||||
// that would be breaking.
|
||||
v.Dir = filepath.Join(v.Dir, filecacheRootDirname, k)
|
||||
} else {
|
||||
v.Dir = filepath.Join(v.Dir, k)
|
||||
}
|
||||
|
||||
if disabled {
|
||||
v.MaxAge = 0
|
||||
}
|
||||
|
||||
c[k] = v
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Resolves :resourceDir => /myproject/resources etc., :cacheDir => ...
|
||||
func resolveDirPlaceholder(fs afero.Fs, cfg config.Provider, placeholder string) (cacheDir string, isResource bool, err error) {
|
||||
workingDir := cfg.GetString("workingDir")
|
||||
|
||||
switch strings.ToLower(placeholder) {
|
||||
case ":resourcedir":
|
||||
return "", true, nil
|
||||
case ":cachedir":
|
||||
d, err := helpers.GetCacheDir(fs, cfg)
|
||||
return d, false, err
|
||||
case ":project":
|
||||
return filepath.Base(workingDir), false, nil
|
||||
}
|
||||
|
||||
return "", false, errors.Errorf("%q is not a valid placeholder (valid values are :cacheDir or :resourceDir)", placeholder)
|
||||
}
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
// Copyright 2018 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 filecache
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
|
||||
"github.com/gohugoio/hugo/config"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDecodeConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert := require.New(t)
|
||||
|
||||
configStr := `
|
||||
resourceDir = "myresources"
|
||||
contentDir = "content"
|
||||
dataDir = "data"
|
||||
i18nDir = "i18n"
|
||||
layoutDir = "layouts"
|
||||
assetDir = "assets"
|
||||
archetypeDir = "archetypes"
|
||||
|
||||
[caches]
|
||||
[caches.getJSON]
|
||||
maxAge = "10m"
|
||||
dir = "/path/to/c1"
|
||||
[caches.getCSV]
|
||||
maxAge = "11h"
|
||||
dir = "/path/to/c2"
|
||||
[caches.images]
|
||||
dir = "/path/to/c3"
|
||||
|
||||
`
|
||||
|
||||
cfg, err := config.FromConfigString(configStr, "toml")
|
||||
assert.NoError(err)
|
||||
fs := afero.NewMemMapFs()
|
||||
decoded, err := DecodeConfig(fs, cfg)
|
||||
assert.NoError(err)
|
||||
|
||||
assert.Equal(5, len(decoded))
|
||||
|
||||
c2 := decoded["getcsv"]
|
||||
assert.Equal("11h0m0s", c2.MaxAge.String())
|
||||
assert.Equal(filepath.FromSlash("/path/to/c2/filecache/getcsv"), c2.Dir)
|
||||
|
||||
c3 := decoded["images"]
|
||||
assert.Equal(time.Duration(-1), c3.MaxAge)
|
||||
assert.Equal(filepath.FromSlash("/path/to/c3/filecache/images"), c3.Dir)
|
||||
|
||||
}
|
||||
|
||||
func TestDecodeConfigIgnoreCache(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert := require.New(t)
|
||||
|
||||
configStr := `
|
||||
resourceDir = "myresources"
|
||||
contentDir = "content"
|
||||
dataDir = "data"
|
||||
i18nDir = "i18n"
|
||||
layoutDir = "layouts"
|
||||
assetDir = "assets"
|
||||
archeTypedir = "archetypes"
|
||||
|
||||
ignoreCache = true
|
||||
[caches]
|
||||
[caches.getJSON]
|
||||
maxAge = 1234
|
||||
dir = "/path/to/c1"
|
||||
[caches.getCSV]
|
||||
maxAge = 3456
|
||||
dir = "/path/to/c2"
|
||||
[caches.images]
|
||||
dir = "/path/to/c3"
|
||||
|
||||
`
|
||||
|
||||
cfg, err := config.FromConfigString(configStr, "toml")
|
||||
assert.NoError(err)
|
||||
fs := afero.NewMemMapFs()
|
||||
decoded, err := DecodeConfig(fs, cfg)
|
||||
assert.NoError(err)
|
||||
|
||||
assert.Equal(5, len(decoded))
|
||||
|
||||
for _, v := range decoded {
|
||||
assert.Equal(time.Duration(0), v.MaxAge)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestDecodeConfigDefault(t *testing.T) {
|
||||
assert := require.New(t)
|
||||
cfg := newTestConfig()
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
cfg.Set("resourceDir", "c:\\cache\\resources")
|
||||
cfg.Set("cacheDir", "c:\\cache\\thecache")
|
||||
|
||||
} else {
|
||||
cfg.Set("resourceDir", "/cache/resources")
|
||||
cfg.Set("cacheDir", "/cache/thecache")
|
||||
}
|
||||
|
||||
fs := afero.NewMemMapFs()
|
||||
|
||||
decoded, err := DecodeConfig(fs, cfg)
|
||||
|
||||
assert.NoError(err)
|
||||
|
||||
assert.Equal(5, len(decoded))
|
||||
|
||||
imgConfig := decoded[cacheKeyImages]
|
||||
jsonConfig := decoded[cacheKeyGetJSON]
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
assert.Equal(filepath.FromSlash("_gen/images"), imgConfig.Dir)
|
||||
} else {
|
||||
assert.Equal("_gen/images", imgConfig.Dir)
|
||||
assert.Equal("/cache/thecache/hugoproject/filecache/getjson", jsonConfig.Dir)
|
||||
}
|
||||
|
||||
assert.True(imgConfig.isResourceDir)
|
||||
assert.False(jsonConfig.isResourceDir)
|
||||
}
|
||||
|
||||
func TestDecodeConfigInvalidDir(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert := require.New(t)
|
||||
|
||||
configStr := `
|
||||
resourceDir = "myresources"
|
||||
contentDir = "content"
|
||||
dataDir = "data"
|
||||
i18nDir = "i18n"
|
||||
layoutDir = "layouts"
|
||||
assetDir = "assets"
|
||||
archeTypedir = "archetypes"
|
||||
|
||||
[caches]
|
||||
[caches.getJSON]
|
||||
maxAge = "10m"
|
||||
dir = "/"
|
||||
|
||||
`
|
||||
if runtime.GOOS == "windows" {
|
||||
configStr = strings.Replace(configStr, "/", "c:\\\\", 1)
|
||||
}
|
||||
|
||||
cfg, err := config.FromConfigString(configStr, "toml")
|
||||
assert.NoError(err)
|
||||
fs := afero.NewMemMapFs()
|
||||
|
||||
_, err = DecodeConfig(fs, cfg)
|
||||
assert.Error(err)
|
||||
|
||||
}
|
||||
|
||||
func newTestConfig() *viper.Viper {
|
||||
cfg := viper.New()
|
||||
cfg.Set("workingDir", filepath.FromSlash("/my/cool/hugoproject"))
|
||||
cfg.Set("contentDir", "content")
|
||||
cfg.Set("dataDir", "data")
|
||||
cfg.Set("resourceDir", "resources")
|
||||
cfg.Set("i18nDir", "i18n")
|
||||
cfg.Set("layoutDir", "layouts")
|
||||
cfg.Set("archetypeDir", "archetypes")
|
||||
cfg.Set("assetDir", "assets")
|
||||
|
||||
return cfg
|
||||
}
|
||||
Vendored
+127
@@ -0,0 +1,127 @@
|
||||
// Copyright 2018 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 filecache
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
// Prune removes expired and unused items from this cache.
|
||||
// The last one requires a full build so the cache usage can be tracked.
|
||||
// Note that we operate directly on the filesystem here, so this is not
|
||||
// thread safe.
|
||||
func (c Caches) Prune() (int, error) {
|
||||
counter := 0
|
||||
for k, cache := range c {
|
||||
|
||||
count, err := cache.Prune(false)
|
||||
|
||||
if err != nil {
|
||||
return counter, errors.Wrapf(err, "failed to prune cache %q", k)
|
||||
}
|
||||
|
||||
counter += count
|
||||
|
||||
}
|
||||
|
||||
return counter, nil
|
||||
}
|
||||
|
||||
// Prune removes expired and unused items from this cache.
|
||||
// If force is set, everything will be removed not considering expiry time.
|
||||
func (c *Cache) Prune(force bool) (int, error) {
|
||||
if c.pruneAllRootDir != "" {
|
||||
return c.pruneRootDir(force)
|
||||
}
|
||||
|
||||
counter := 0
|
||||
|
||||
err := afero.Walk(c.Fs, "", func(name string, info os.FileInfo, err error) error {
|
||||
if info == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
name = cleanID(name)
|
||||
|
||||
if info.IsDir() {
|
||||
f, err := c.Fs.Open(name)
|
||||
if err != nil {
|
||||
// This cache dir may not exist.
|
||||
return nil
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = f.Readdirnames(1)
|
||||
if err == io.EOF {
|
||||
// Empty dir.
|
||||
return c.Fs.Remove(name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
shouldRemove := force || c.isExpired(info.ModTime())
|
||||
|
||||
if !shouldRemove && len(c.nlocker.seen) > 0 {
|
||||
// Remove it if it's not been touched/used in the last build.
|
||||
_, seen := c.nlocker.seen[name]
|
||||
shouldRemove = !seen
|
||||
}
|
||||
|
||||
if shouldRemove {
|
||||
err := c.Fs.Remove(name)
|
||||
if err == nil {
|
||||
counter++
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return counter, err
|
||||
}
|
||||
|
||||
func (c *Cache) pruneRootDir(force bool) (int, error) {
|
||||
|
||||
info, err := c.Fs.Stat(c.pruneAllRootDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if !force && !c.isExpired(info.ModTime()) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
counter := 0
|
||||
// Module cache has 0555 directories; make them writable in order to remove content.
|
||||
afero.Walk(c.Fs, c.pruneAllRootDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if info.IsDir() {
|
||||
counter++
|
||||
c.Fs.Chmod(path, 0777)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return 1, c.Fs.RemoveAll(c.pruneAllRootDir)
|
||||
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
// Copyright 2018 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 filecache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPrune(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert := require.New(t)
|
||||
|
||||
configStr := `
|
||||
resourceDir = "myresources"
|
||||
contentDir = "content"
|
||||
dataDir = "data"
|
||||
i18nDir = "i18n"
|
||||
layoutDir = "layouts"
|
||||
assetDir = "assets"
|
||||
archeTypedir = "archetypes"
|
||||
|
||||
[caches]
|
||||
[caches.getjson]
|
||||
maxAge = "200ms"
|
||||
dir = "/cache/c"
|
||||
[caches.getcsv]
|
||||
maxAge = "200ms"
|
||||
dir = "/cache/d"
|
||||
[caches.assets]
|
||||
maxAge = "200ms"
|
||||
dir = ":resourceDir/_gen"
|
||||
[caches.images]
|
||||
maxAge = "200ms"
|
||||
dir = ":resourceDir/_gen"
|
||||
`
|
||||
|
||||
for _, name := range []string{cacheKeyGetCSV, cacheKeyGetJSON, cacheKeyAssets, cacheKeyImages} {
|
||||
msg := fmt.Sprintf("cache: %s", name)
|
||||
p := newPathsSpec(t, afero.NewMemMapFs(), configStr)
|
||||
caches, err := NewCaches(p)
|
||||
assert.NoError(err)
|
||||
cache := caches[name]
|
||||
for i := 0; i < 10; i++ {
|
||||
id := fmt.Sprintf("i%d", i)
|
||||
cache.GetOrCreateBytes(id, func() ([]byte, error) {
|
||||
return []byte("abc"), nil
|
||||
})
|
||||
if i == 4 {
|
||||
// This will expire the first 5
|
||||
time.Sleep(201 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
count, err := caches.Prune()
|
||||
assert.NoError(err)
|
||||
assert.Equal(5, count, msg)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
id := fmt.Sprintf("i%d", i)
|
||||
v := cache.getString(id)
|
||||
if i < 5 {
|
||||
assert.Equal("", v, id)
|
||||
} else {
|
||||
assert.Equal("abc", v, id)
|
||||
}
|
||||
}
|
||||
|
||||
caches, err = NewCaches(p)
|
||||
assert.NoError(err)
|
||||
cache = caches[name]
|
||||
// Touch one and then prune.
|
||||
cache.GetOrCreateBytes("i5", func() ([]byte, error) {
|
||||
return []byte("abc"), nil
|
||||
})
|
||||
|
||||
count, err = caches.Prune()
|
||||
assert.NoError(err)
|
||||
assert.Equal(4, count)
|
||||
|
||||
// Now only the i5 should be left.
|
||||
for i := 0; i < 10; i++ {
|
||||
id := fmt.Sprintf("i%d", i)
|
||||
v := cache.getString(id)
|
||||
if i != 5 {
|
||||
assert.Equal("", v, id)
|
||||
} else {
|
||||
assert.Equal("abc", v, id)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Vendored
+299
@@ -0,0 +1,299 @@
|
||||
// Copyright 2018 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 filecache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/langs"
|
||||
"github.com/gohugoio/hugo/modules"
|
||||
|
||||
"github.com/gohugoio/hugo/common/hugio"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
"github.com/spf13/afero"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestFileCache(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert := require.New(t)
|
||||
|
||||
tempWorkingDir, err := ioutil.TempDir("", "hugo_filecache_test_work")
|
||||
assert.NoError(err)
|
||||
defer os.Remove(tempWorkingDir)
|
||||
|
||||
tempCacheDir, err := ioutil.TempDir("", "hugo_filecache_test_cache")
|
||||
assert.NoError(err)
|
||||
defer os.Remove(tempCacheDir)
|
||||
|
||||
osfs := afero.NewOsFs()
|
||||
|
||||
for _, test := range []struct {
|
||||
cacheDir string
|
||||
workingDir string
|
||||
}{
|
||||
// Run with same dirs twice to make sure that works.
|
||||
{tempCacheDir, tempWorkingDir},
|
||||
{tempCacheDir, tempWorkingDir},
|
||||
} {
|
||||
|
||||
configStr := `
|
||||
workingDir = "WORKING_DIR"
|
||||
resourceDir = "resources"
|
||||
cacheDir = "CACHEDIR"
|
||||
contentDir = "content"
|
||||
dataDir = "data"
|
||||
i18nDir = "i18n"
|
||||
layoutDir = "layouts"
|
||||
assetDir = "assets"
|
||||
archeTypedir = "archetypes"
|
||||
|
||||
[caches]
|
||||
[caches.getJSON]
|
||||
maxAge = "10h"
|
||||
dir = ":cacheDir/c"
|
||||
|
||||
`
|
||||
|
||||
winPathSep := "\\\\"
|
||||
|
||||
replacer := strings.NewReplacer("CACHEDIR", test.cacheDir, "WORKING_DIR", test.workingDir)
|
||||
|
||||
configStr = replacer.Replace(configStr)
|
||||
configStr = strings.Replace(configStr, "\\", winPathSep, -1)
|
||||
|
||||
p := newPathsSpec(t, osfs, configStr)
|
||||
|
||||
caches, err := NewCaches(p)
|
||||
assert.NoError(err)
|
||||
|
||||
cache := caches.Get("GetJSON")
|
||||
assert.NotNil(cache)
|
||||
assert.Equal("10h0m0s", cache.maxAge.String())
|
||||
|
||||
bfs, ok := cache.Fs.(*afero.BasePathFs)
|
||||
assert.True(ok)
|
||||
filename, err := bfs.RealPath("key")
|
||||
assert.NoError(err)
|
||||
if test.cacheDir != "" {
|
||||
assert.Equal(filepath.Join(test.cacheDir, "c/"+filecacheRootDirname+"/getjson/key"), filename)
|
||||
} else {
|
||||
// Temp dir.
|
||||
assert.Regexp(regexp.MustCompile(".*hugo_cache.*"+filecacheRootDirname+".*key"), filename)
|
||||
}
|
||||
|
||||
cache = caches.Get("Images")
|
||||
assert.NotNil(cache)
|
||||
assert.Equal(time.Duration(-1), cache.maxAge)
|
||||
bfs, ok = cache.Fs.(*afero.BasePathFs)
|
||||
assert.True(ok)
|
||||
filename, _ = bfs.RealPath("key")
|
||||
assert.Equal(filepath.FromSlash("_gen/images/key"), filename)
|
||||
|
||||
rf := func(s string) func() (io.ReadCloser, error) {
|
||||
return func() (io.ReadCloser, error) {
|
||||
return struct {
|
||||
io.ReadSeeker
|
||||
io.Closer
|
||||
}{
|
||||
strings.NewReader(s),
|
||||
ioutil.NopCloser(nil),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
bf := func() ([]byte, error) {
|
||||
return []byte("bcd"), nil
|
||||
}
|
||||
|
||||
for _, c := range []*Cache{caches.ImageCache(), caches.AssetsCache(), caches.GetJSONCache(), caches.GetCSVCache()} {
|
||||
for i := 0; i < 2; i++ {
|
||||
info, r, err := c.GetOrCreate("a", rf("abc"))
|
||||
assert.NoError(err)
|
||||
assert.NotNil(r)
|
||||
assert.Equal("a", info.Name)
|
||||
b, _ := ioutil.ReadAll(r)
|
||||
r.Close()
|
||||
assert.Equal("abc", string(b))
|
||||
|
||||
info, b, err = c.GetOrCreateBytes("b", bf)
|
||||
assert.NoError(err)
|
||||
assert.NotNil(r)
|
||||
assert.Equal("b", info.Name)
|
||||
assert.Equal("bcd", string(b))
|
||||
|
||||
_, b, err = c.GetOrCreateBytes("a", bf)
|
||||
assert.NoError(err)
|
||||
assert.Equal("abc", string(b))
|
||||
|
||||
_, r, err = c.GetOrCreate("a", rf("bcd"))
|
||||
assert.NoError(err)
|
||||
b, _ = ioutil.ReadAll(r)
|
||||
r.Close()
|
||||
assert.Equal("abc", string(b))
|
||||
}
|
||||
}
|
||||
|
||||
assert.NotNil(caches.Get("getJSON"))
|
||||
|
||||
info, w, err := caches.ImageCache().WriteCloser("mykey")
|
||||
assert.NoError(err)
|
||||
assert.Equal("mykey", info.Name)
|
||||
io.WriteString(w, "Hugo is great!")
|
||||
w.Close()
|
||||
assert.Equal("Hugo is great!", caches.ImageCache().getString("mykey"))
|
||||
|
||||
info, r, err := caches.ImageCache().Get("mykey")
|
||||
assert.NoError(err)
|
||||
assert.NotNil(r)
|
||||
assert.Equal("mykey", info.Name)
|
||||
b, _ := ioutil.ReadAll(r)
|
||||
r.Close()
|
||||
assert.Equal("Hugo is great!", string(b))
|
||||
|
||||
info, b, err = caches.ImageCache().GetBytes("mykey")
|
||||
assert.NoError(err)
|
||||
assert.Equal("mykey", info.Name)
|
||||
assert.Equal("Hugo is great!", string(b))
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestFileCacheConcurrent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert := require.New(t)
|
||||
|
||||
configStr := `
|
||||
resourceDir = "myresources"
|
||||
contentDir = "content"
|
||||
dataDir = "data"
|
||||
i18nDir = "i18n"
|
||||
layoutDir = "layouts"
|
||||
assetDir = "assets"
|
||||
archeTypedir = "archetypes"
|
||||
|
||||
[caches]
|
||||
[caches.getjson]
|
||||
maxAge = "1s"
|
||||
dir = "/cache/c"
|
||||
|
||||
`
|
||||
|
||||
p := newPathsSpec(t, afero.NewMemMapFs(), configStr)
|
||||
|
||||
caches, err := NewCaches(p)
|
||||
assert.NoError(err)
|
||||
|
||||
const cacheName = "getjson"
|
||||
|
||||
filenameData := func(i int) (string, string) {
|
||||
data := fmt.Sprintf("data: %d", i)
|
||||
filename := fmt.Sprintf("file%d", i)
|
||||
return filename, data
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for i := 0; i < 50; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
for j := 0; j < 20; j++ {
|
||||
c := caches.Get(cacheName)
|
||||
assert.NotNil(c)
|
||||
filename, data := filenameData(i)
|
||||
_, r, err := c.GetOrCreate(filename, func() (io.ReadCloser, error) {
|
||||
return hugio.ToReadCloser(strings.NewReader(data)), nil
|
||||
})
|
||||
assert.NoError(err)
|
||||
b, _ := ioutil.ReadAll(r)
|
||||
r.Close()
|
||||
assert.Equal(data, string(b))
|
||||
// Trigger some expiration.
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
}(i)
|
||||
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestCleanID(t *testing.T) {
|
||||
assert := require.New(t)
|
||||
assert.Equal(filepath.FromSlash("a/b/c.txt"), cleanID(filepath.FromSlash("/a/b//c.txt")))
|
||||
assert.Equal(filepath.FromSlash("a/b/c.txt"), cleanID(filepath.FromSlash("a/b//c.txt")))
|
||||
}
|
||||
|
||||
func initConfig(fs afero.Fs, cfg config.Provider) error {
|
||||
if _, err := langs.LoadLanguageSettings(cfg, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
modConfig, err := modules.DecodeConfig(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
workingDir := cfg.GetString("workingDir")
|
||||
themesDir := cfg.GetString("themesDir")
|
||||
if !filepath.IsAbs(themesDir) {
|
||||
themesDir = filepath.Join(workingDir, themesDir)
|
||||
}
|
||||
modulesClient := modules.NewClient(modules.ClientConfig{
|
||||
Fs: fs,
|
||||
WorkingDir: workingDir,
|
||||
ThemesDir: themesDir,
|
||||
ModuleConfig: modConfig,
|
||||
IgnoreVendor: true,
|
||||
})
|
||||
|
||||
moduleConfig, err := modulesClient.Collect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := modules.ApplyProjectConfigDefaults(cfg, moduleConfig.ActiveModules[len(moduleConfig.ActiveModules)-1]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg.Set("allModules", moduleConfig.ActiveModules)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func newPathsSpec(t *testing.T, fs afero.Fs, configStr string) *helpers.PathSpec {
|
||||
assert := require.New(t)
|
||||
cfg, err := config.FromConfigString(configStr, "toml")
|
||||
assert.NoError(err)
|
||||
initConfig(fs, cfg)
|
||||
p, err := helpers.NewPathSpec(hugofs.NewFrom(fs, cfg), cfg, nil)
|
||||
assert.NoError(err)
|
||||
return p
|
||||
|
||||
}
|
||||
Vendored
+79
@@ -0,0 +1,79 @@
|
||||
// Copyright 2018 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 namedmemcache provides a memory cache with a named lock. This is suitable
|
||||
// for situations where creating the cached resource can be time consuming or otherwise
|
||||
// resource hungry, or in situations where a "once only per key" is a requirement.
|
||||
package namedmemcache
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/BurntSushi/locker"
|
||||
)
|
||||
|
||||
// Cache holds the cached values.
|
||||
type Cache struct {
|
||||
nlocker *locker.Locker
|
||||
cache map[string]cacheEntry
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
type cacheEntry struct {
|
||||
value interface{}
|
||||
err error
|
||||
}
|
||||
|
||||
// New creates a new cache.
|
||||
func New() *Cache {
|
||||
return &Cache{
|
||||
nlocker: locker.NewLocker(),
|
||||
cache: make(map[string]cacheEntry),
|
||||
}
|
||||
}
|
||||
|
||||
// Clear clears the cache state.
|
||||
func (c *Cache) Clear() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
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.
|
||||
func (c *Cache) GetOrCreate(key string, create func() (interface{}, error)) (interface{}, error) {
|
||||
c.mu.RLock()
|
||||
entry, found := c.cache[key]
|
||||
c.mu.RUnlock()
|
||||
|
||||
if found {
|
||||
return entry.value, entry.err
|
||||
}
|
||||
|
||||
c.nlocker.Lock(key)
|
||||
defer c.nlocker.Unlock(key)
|
||||
|
||||
// Create it.
|
||||
value, err := create()
|
||||
|
||||
c.mu.Lock()
|
||||
c.cache[key] = cacheEntry{value: value, err: err}
|
||||
c.mu.Unlock()
|
||||
|
||||
return value, err
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
// Copyright 2018 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 namedmemcache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNamedCache(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert := require.New(t)
|
||||
|
||||
cache := New()
|
||||
|
||||
counter := 0
|
||||
create := func() (interface{}, error) {
|
||||
counter++
|
||||
return counter, nil
|
||||
}
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
v1, err := cache.GetOrCreate("a1", create)
|
||||
assert.NoError(err)
|
||||
assert.Equal(1, v1)
|
||||
v2, err := cache.GetOrCreate("a2", create)
|
||||
assert.NoError(err)
|
||||
assert.Equal(2, v2)
|
||||
}
|
||||
|
||||
cache.Clear()
|
||||
|
||||
v3, err := cache.GetOrCreate("a2", create)
|
||||
assert.NoError(err)
|
||||
assert.Equal(3, v3)
|
||||
}
|
||||
|
||||
func TestNamedCacheConcurrent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert := require.New(t)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
cache := New()
|
||||
|
||||
create := func(i int) func() (interface{}, error) {
|
||||
return func() (interface{}, error) {
|
||||
return i, nil
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for j := 0; j < 100; j++ {
|
||||
id := fmt.Sprintf("id%d", j)
|
||||
v, err := cache.GetOrCreate(id, create(j))
|
||||
assert.NoError(err)
|
||||
assert.Equal(j, v)
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
@@ -0,0 +1,548 @@
|
||||
// Copyright 2019 The Hugo Authors. All rights reserved.
|
||||
// Some functions in this file (see comments) is based on the Go source code,
|
||||
// copyright The Go Authors and governed by a BSD-style license.
|
||||
//
|
||||
// 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 codegen contains helpers for code generation.
|
||||
package codegen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Make room for insertions
|
||||
const weightWidth = 1000
|
||||
|
||||
// NewInspector creates a new Inspector given a source root.
|
||||
func NewInspector(root string) *Inspector {
|
||||
return &Inspector{ProjectRootDir: root}
|
||||
}
|
||||
|
||||
// Inspector provides methods to help code generation. It uses a combination
|
||||
// of reflection and source code AST to do the heavy lifting.
|
||||
type Inspector struct {
|
||||
ProjectRootDir string
|
||||
|
||||
init sync.Once
|
||||
|
||||
// Determines method order. Go's reflect sorts lexicographically, so
|
||||
// we must parse the source to preserve this order.
|
||||
methodWeight map[string]map[string]int
|
||||
}
|
||||
|
||||
// MethodsFromTypes create a method set from the include slice, excluding any
|
||||
// method in exclude.
|
||||
func (c *Inspector) MethodsFromTypes(include []reflect.Type, exclude []reflect.Type) Methods {
|
||||
c.parseSource()
|
||||
|
||||
var methods Methods
|
||||
|
||||
var excludes = make(map[string]bool)
|
||||
|
||||
if len(exclude) > 0 {
|
||||
for _, m := range c.MethodsFromTypes(exclude, nil) {
|
||||
excludes[m.Name] = true
|
||||
}
|
||||
}
|
||||
|
||||
// There may be overlapping interfaces in types. Do a simple check for now.
|
||||
seen := make(map[string]bool)
|
||||
|
||||
nameAndPackage := func(t reflect.Type) (string, string) {
|
||||
var name, pkg string
|
||||
|
||||
isPointer := t.Kind() == reflect.Ptr
|
||||
|
||||
if isPointer {
|
||||
t = t.Elem()
|
||||
}
|
||||
|
||||
pkgPrefix := ""
|
||||
if pkgPath := t.PkgPath(); pkgPath != "" {
|
||||
pkgPath = strings.TrimSuffix(pkgPath, "/")
|
||||
_, shortPath := path.Split(pkgPath)
|
||||
pkgPrefix = shortPath + "."
|
||||
pkg = pkgPath
|
||||
}
|
||||
|
||||
name = t.Name()
|
||||
if name == "" {
|
||||
// interface{}
|
||||
name = t.String()
|
||||
}
|
||||
|
||||
if isPointer {
|
||||
pkgPrefix = "*" + pkgPrefix
|
||||
}
|
||||
|
||||
name = pkgPrefix + name
|
||||
|
||||
return name, pkg
|
||||
|
||||
}
|
||||
|
||||
for _, t := range include {
|
||||
|
||||
for i := 0; i < t.NumMethod(); i++ {
|
||||
|
||||
m := t.Method(i)
|
||||
if excludes[m.Name] || seen[m.Name] {
|
||||
continue
|
||||
}
|
||||
|
||||
seen[m.Name] = true
|
||||
|
||||
if m.PkgPath != "" {
|
||||
// Not exported
|
||||
continue
|
||||
}
|
||||
|
||||
numIn := m.Type.NumIn()
|
||||
|
||||
ownerName, _ := nameAndPackage(t)
|
||||
|
||||
method := Method{Owner: t, OwnerName: ownerName, Name: m.Name}
|
||||
|
||||
for i := 0; i < numIn; i++ {
|
||||
in := m.Type.In(i)
|
||||
|
||||
name, pkg := nameAndPackage(in)
|
||||
|
||||
if pkg != "" {
|
||||
method.Imports = append(method.Imports, pkg)
|
||||
}
|
||||
|
||||
method.In = append(method.In, name)
|
||||
}
|
||||
|
||||
numOut := m.Type.NumOut()
|
||||
|
||||
if numOut > 0 {
|
||||
for i := 0; i < numOut; i++ {
|
||||
out := m.Type.Out(i)
|
||||
name, pkg := nameAndPackage(out)
|
||||
|
||||
if pkg != "" {
|
||||
method.Imports = append(method.Imports, pkg)
|
||||
}
|
||||
|
||||
method.Out = append(method.Out, name)
|
||||
}
|
||||
}
|
||||
|
||||
methods = append(methods, method)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
sort.SliceStable(methods, func(i, j int) bool {
|
||||
mi, mj := methods[i], methods[j]
|
||||
|
||||
wi := c.methodWeight[mi.OwnerName][mi.Name]
|
||||
wj := c.methodWeight[mj.OwnerName][mj.Name]
|
||||
|
||||
if wi == wj {
|
||||
return mi.Name < mj.Name
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
c.methodWeight = make(map[string]map[string]int)
|
||||
dirExcludes := regexp.MustCompile("docs|examples")
|
||||
fileExcludes := regexp.MustCompile("autogen")
|
||||
var filenames []string
|
||||
|
||||
filepath.Walk(c.ProjectRootDir, func(path string, info os.FileInfo, err error) error {
|
||||
if info.IsDir() {
|
||||
if dirExcludes.MatchString(info.Name()) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(path, ".go") || fileExcludes.MatchString(path) {
|
||||
return nil
|
||||
}
|
||||
|
||||
filenames = append(filenames, path)
|
||||
|
||||
return nil
|
||||
|
||||
})
|
||||
|
||||
for _, filename := range filenames {
|
||||
|
||||
pkg := c.packageFromPath(filename)
|
||||
|
||||
fset := token.NewFileSet()
|
||||
node, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
ast.Inspect(node, func(n ast.Node) bool {
|
||||
switch t := n.(type) {
|
||||
case *ast.TypeSpec:
|
||||
if t.Name.IsExported() {
|
||||
switch it := t.Type.(type) {
|
||||
case *ast.InterfaceType:
|
||||
iface := pkg + "." + t.Name.Name
|
||||
methodNames := collectMethodsRecursive(pkg, it.Methods.List)
|
||||
weights := make(map[string]int)
|
||||
weight := weightWidth
|
||||
for _, name := range methodNames {
|
||||
weights[name] = weight
|
||||
weight += weightWidth
|
||||
}
|
||||
c.methodWeight[iface] = weights
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// Complement
|
||||
for _, v1 := range c.methodWeight {
|
||||
for k2, w := range v1 {
|
||||
if v, found := c.methodWeight[k2]; found {
|
||||
for k3, v3 := range v {
|
||||
v1[k3] = (v3 / weightWidth) + w
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Inspector) packageFromPath(p string) string {
|
||||
p = filepath.ToSlash(p)
|
||||
base := path.Base(p)
|
||||
if !strings.Contains(base, ".") {
|
||||
return base
|
||||
}
|
||||
return path.Base(strings.TrimSuffix(p, base))
|
||||
}
|
||||
|
||||
// Method holds enough information about it to recreate it.
|
||||
type Method struct {
|
||||
// The interface we extracted this method from.
|
||||
Owner reflect.Type
|
||||
|
||||
// String version of the above, on the form PACKAGE.NAME, e.g.
|
||||
// page.Page
|
||||
OwnerName string
|
||||
|
||||
// Method name.
|
||||
Name string
|
||||
|
||||
// Imports needed to satisfy the method signature.
|
||||
Imports []string
|
||||
|
||||
// Argument types, including any package prefix, e.g. string, int, interface{},
|
||||
// net.Url
|
||||
In []string
|
||||
|
||||
// Return types.
|
||||
Out []string
|
||||
}
|
||||
|
||||
// Declaration creates a method declaration (without any body) for the given receiver.
|
||||
func (m Method) Declaration(receiver string) string {
|
||||
return fmt.Sprintf("func (%s %s) %s%s %s", receiverShort(receiver), receiver, m.Name, m.inStr(), m.outStr())
|
||||
}
|
||||
|
||||
// DeclarationNamed creates a method declaration (without any body) for the given receiver
|
||||
// with named return values.
|
||||
func (m Method) DeclarationNamed(receiver string) string {
|
||||
return fmt.Sprintf("func (%s %s) %s%s %s", receiverShort(receiver), receiver, m.Name, m.inStr(), m.outStrNamed())
|
||||
}
|
||||
|
||||
// Delegate creates a delegate call string.
|
||||
func (m Method) Delegate(receiver, delegate string) string {
|
||||
ret := ""
|
||||
if len(m.Out) > 0 {
|
||||
ret = "return "
|
||||
}
|
||||
return fmt.Sprintf("%s%s.%s.%s%s", ret, receiverShort(receiver), delegate, m.Name, m.inOutStr())
|
||||
}
|
||||
|
||||
func (m Method) String() string {
|
||||
return m.Name + m.inStr() + " " + m.outStr() + "\n"
|
||||
}
|
||||
|
||||
func (m Method) inOutStr() string {
|
||||
if len(m.In) == 0 {
|
||||
return "()"
|
||||
}
|
||||
|
||||
args := make([]string, len(m.In))
|
||||
for i := 0; i < len(args); i++ {
|
||||
args[i] = fmt.Sprintf("arg%d", i)
|
||||
}
|
||||
return "(" + strings.Join(args, ", ") + ")"
|
||||
}
|
||||
|
||||
func (m Method) inStr() string {
|
||||
if len(m.In) == 0 {
|
||||
return "()"
|
||||
}
|
||||
|
||||
args := make([]string, len(m.In))
|
||||
for i := 0; i < len(args); i++ {
|
||||
args[i] = fmt.Sprintf("arg%d %s", i, m.In[i])
|
||||
}
|
||||
return "(" + strings.Join(args, ", ") + ")"
|
||||
}
|
||||
|
||||
func (m Method) outStr() string {
|
||||
if len(m.Out) == 0 {
|
||||
return ""
|
||||
}
|
||||
if len(m.Out) == 1 {
|
||||
return m.Out[0]
|
||||
}
|
||||
|
||||
return "(" + strings.Join(m.Out, ", ") + ")"
|
||||
}
|
||||
|
||||
func (m Method) outStrNamed() string {
|
||||
if len(m.Out) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
outs := make([]string, len(m.Out))
|
||||
for i := 0; i < len(outs); i++ {
|
||||
outs[i] = fmt.Sprintf("o%d %s", i, m.Out[i])
|
||||
}
|
||||
|
||||
return "(" + strings.Join(outs, ", ") + ")"
|
||||
}
|
||||
|
||||
// Methods represents a list of methods for one or more interfaces.
|
||||
// The order matches the defined order in their source file(s).
|
||||
type Methods []Method
|
||||
|
||||
// Imports returns a sorted list of package imports needed to satisfy the
|
||||
// signatures of all methods.
|
||||
func (m Methods) Imports() []string {
|
||||
var pkgImports []string
|
||||
for _, method := range m {
|
||||
pkgImports = append(pkgImports, method.Imports...)
|
||||
}
|
||||
if len(pkgImports) > 0 {
|
||||
pkgImports = uniqueNonEmptyStrings(pkgImports)
|
||||
sort.Strings(pkgImports)
|
||||
}
|
||||
return pkgImports
|
||||
}
|
||||
|
||||
// ToMarshalJSON creates a MarshalJSON method for these methods. Any method name
|
||||
// matchin any of the regexps in excludes will be ignored.
|
||||
func (m Methods) ToMarshalJSON(receiver, pkgPath string, excludes ...string) (string, []string) {
|
||||
var sb strings.Builder
|
||||
|
||||
r := receiverShort(receiver)
|
||||
what := firstToUpper(trimAsterisk(receiver))
|
||||
pgkName := path.Base(pkgPath)
|
||||
|
||||
fmt.Fprintf(&sb, "func Marshal%sToJSON(%s %s) ([]byte, error) {\n", what, r, receiver)
|
||||
|
||||
var methods Methods
|
||||
var excludeRes = make([]*regexp.Regexp, len(excludes))
|
||||
|
||||
for i, exclude := range excludes {
|
||||
excludeRes[i] = regexp.MustCompile(exclude)
|
||||
}
|
||||
|
||||
for _, method := range m {
|
||||
// Exclude methods with arguments and incompatible return values
|
||||
if len(method.In) > 0 || len(method.Out) == 0 || len(method.Out) > 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(method.Out) == 2 {
|
||||
if method.Out[1] != "error" {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
for _, re := range excludeRes {
|
||||
if re.MatchString(method.Name) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
methods = append(methods, method)
|
||||
}
|
||||
|
||||
for _, method := range methods {
|
||||
varn := varName(method.Name)
|
||||
if len(method.Out) == 1 {
|
||||
fmt.Fprintf(&sb, "\t%s := %s.%s()\n", varn, r, method.Name)
|
||||
} else {
|
||||
fmt.Fprintf(&sb, "\t%s, err := %s.%s()\n", varn, r, method.Name)
|
||||
fmt.Fprint(&sb, "\tif err != nil {\n\t\treturn nil, err\n\t}\n")
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprint(&sb, "\n\ts := struct {\n")
|
||||
|
||||
for _, method := range methods {
|
||||
fmt.Fprintf(&sb, "\t\t%s %s\n", method.Name, typeName(method.Out[0], pgkName))
|
||||
}
|
||||
|
||||
fmt.Fprint(&sb, "\n\t}{\n")
|
||||
|
||||
for _, method := range methods {
|
||||
varn := varName(method.Name)
|
||||
fmt.Fprintf(&sb, "\t\t%s: %s,\n", method.Name, varn)
|
||||
}
|
||||
|
||||
fmt.Fprint(&sb, "\n\t}\n\n")
|
||||
fmt.Fprint(&sb, "\treturn json.Marshal(&s)\n}")
|
||||
|
||||
pkgImports := append(methods.Imports(), "encoding/json")
|
||||
|
||||
if pkgPath != "" {
|
||||
// Exclude self
|
||||
for i, pkgImp := range pkgImports {
|
||||
if pkgImp == pkgPath {
|
||||
pkgImports = append(pkgImports[:i], pkgImports[i+1:]...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sb.String(), pkgImports
|
||||
|
||||
}
|
||||
|
||||
func collectMethodsRecursive(pkg string, f []*ast.Field) []string {
|
||||
var methodNames []string
|
||||
for _, m := range f {
|
||||
if m.Names != nil {
|
||||
methodNames = append(methodNames, m.Names[0].Name)
|
||||
continue
|
||||
}
|
||||
|
||||
if ident, ok := m.Type.(*ast.Ident); ok && ident.Obj != nil {
|
||||
// Embedded interface
|
||||
methodNames = append(
|
||||
methodNames,
|
||||
collectMethodsRecursive(
|
||||
pkg,
|
||||
ident.Obj.Decl.(*ast.TypeSpec).Type.(*ast.InterfaceType).Methods.List)...)
|
||||
} else {
|
||||
// Embedded, but in a different file/package. Return the
|
||||
// package.Name and deal with that later.
|
||||
name := packageName(m.Type)
|
||||
if !strings.Contains(name, ".") {
|
||||
// Assume current package
|
||||
name = pkg + "." + name
|
||||
}
|
||||
methodNames = append(methodNames, name)
|
||||
}
|
||||
}
|
||||
|
||||
return methodNames
|
||||
|
||||
}
|
||||
|
||||
func firstToLower(name string) string {
|
||||
return strings.ToLower(name[:1]) + name[1:]
|
||||
}
|
||||
|
||||
func firstToUpper(name string) string {
|
||||
return strings.ToUpper(name[:1]) + name[1:]
|
||||
}
|
||||
|
||||
func packageName(e ast.Expr) string {
|
||||
switch tp := e.(type) {
|
||||
case *ast.Ident:
|
||||
return tp.Name
|
||||
case *ast.SelectorExpr:
|
||||
return fmt.Sprintf("%s.%s", packageName(tp.X), packageName(tp.Sel))
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func receiverShort(receiver string) string {
|
||||
return strings.ToLower(trimAsterisk(receiver))[:1]
|
||||
}
|
||||
|
||||
func trimAsterisk(name string) string {
|
||||
return strings.TrimPrefix(name, "*")
|
||||
}
|
||||
|
||||
func typeName(name, pkg string) string {
|
||||
return strings.TrimPrefix(name, pkg+".")
|
||||
}
|
||||
|
||||
func uniqueNonEmptyStrings(s []string) []string {
|
||||
var unique []string
|
||||
set := map[string]interface{}{}
|
||||
for _, val := range s {
|
||||
if val == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := set[val]; !ok {
|
||||
unique = append(unique, val)
|
||||
set[val] = val
|
||||
}
|
||||
}
|
||||
return unique
|
||||
}
|
||||
|
||||
func varName(name string) string {
|
||||
name = firstToLower(name)
|
||||
|
||||
// Adjust some reserved keywords, see https://golang.org/ref/spec#Keywords
|
||||
switch name {
|
||||
case "type":
|
||||
name = "typ"
|
||||
case "package":
|
||||
name = "pkg"
|
||||
// Not reserved, but syntax highlighters has it as a keyword.
|
||||
case "len":
|
||||
name = "length"
|
||||
}
|
||||
|
||||
return name
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2017-present The Hugo Authors. All rights reserved.
|
||||
// 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.
|
||||
@@ -11,13 +11,10 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package hugolib
|
||||
package codegen
|
||||
|
||||
import (
|
||||
"github.com/gohugoio/hugo/resource"
|
||||
)
|
||||
|
||||
var (
|
||||
_ resource.Resource = (*Page)(nil)
|
||||
_ resource.Resource = (*PageOutput)(nil)
|
||||
)
|
||||
type IEmbed interface {
|
||||
MethodEmbed3(s string) string
|
||||
MethodEmbed1() string
|
||||
MethodEmbed2()
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
// 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.
|
||||
|
||||
package codegen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/gohugoio/hugo/common/herrors"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMethods(t *testing.T) {
|
||||
|
||||
var (
|
||||
zeroIE = reflect.TypeOf((*IEmbed)(nil)).Elem()
|
||||
zeroIEOnly = reflect.TypeOf((*IEOnly)(nil)).Elem()
|
||||
zeroI = reflect.TypeOf((*I)(nil)).Elem()
|
||||
)
|
||||
|
||||
dir, _ := os.Getwd()
|
||||
c := NewInspector(dir)
|
||||
|
||||
t.Run("MethodsFromTypes", func(t *testing.T) {
|
||||
assert := require.New(t)
|
||||
|
||||
methods := c.MethodsFromTypes([]reflect.Type{zeroI}, nil)
|
||||
|
||||
methodsStr := fmt.Sprint(methods)
|
||||
|
||||
assert.Contains(methodsStr, "Method1(arg0 herrors.ErrorContext)")
|
||||
assert.Contains(methodsStr, "Method7() interface {}")
|
||||
assert.Contains(methodsStr, "Method0() string\n Method4() string")
|
||||
assert.Contains(methodsStr, "MethodEmbed3(arg0 string) string\n MethodEmbed1() string")
|
||||
|
||||
assert.Contains(methods.Imports(), "github.com/gohugoio/hugo/common/herrors")
|
||||
})
|
||||
|
||||
t.Run("EmbedOnly", func(t *testing.T) {
|
||||
assert := require.New(t)
|
||||
|
||||
methods := c.MethodsFromTypes([]reflect.Type{zeroIEOnly}, nil)
|
||||
|
||||
methodsStr := fmt.Sprint(methods)
|
||||
|
||||
assert.Contains(methodsStr, "MethodEmbed3(arg0 string) string")
|
||||
|
||||
})
|
||||
|
||||
t.Run("ToMarshalJSON", func(t *testing.T) {
|
||||
assert := require.New(t)
|
||||
|
||||
m, pkg := c.MethodsFromTypes(
|
||||
[]reflect.Type{zeroI},
|
||||
[]reflect.Type{zeroIE}).ToMarshalJSON("*page", "page")
|
||||
|
||||
assert.Contains(m, "method6 := p.Method6()")
|
||||
assert.Contains(m, "Method0: method0,")
|
||||
assert.Contains(m, "return json.Marshal(&s)")
|
||||
|
||||
assert.Contains(pkg, "github.com/gohugoio/hugo/common/herrors")
|
||||
assert.Contains(pkg, "encoding/json")
|
||||
|
||||
fmt.Println(pkg)
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
type I interface {
|
||||
IEmbed
|
||||
Method0() string
|
||||
Method4() string
|
||||
Method1(myerr herrors.ErrorContext)
|
||||
Method3(myint int, mystring string)
|
||||
Method5() (string, error)
|
||||
Method6() *net.IP
|
||||
Method7() interface{}
|
||||
Method8() herrors.ErrorContext
|
||||
method2()
|
||||
method9() os.FileInfo
|
||||
}
|
||||
|
||||
type IEOnly interface {
|
||||
IEmbed
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
// Copyright 2015 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 (
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
)
|
||||
|
||||
type benchmarkCmd struct {
|
||||
benchmarkTimes int
|
||||
cpuProfileFile string
|
||||
memProfileFile string
|
||||
|
||||
*baseBuilderCmd
|
||||
}
|
||||
|
||||
func (b *commandsBuilder) newBenchmarkCmd() *benchmarkCmd {
|
||||
cmd := &cobra.Command{
|
||||
Use: "benchmark",
|
||||
Short: "Benchmark Hugo by building a site a number of times.",
|
||||
Long: `Hugo can build a site many times over and analyze the running process
|
||||
creating a benchmark.`,
|
||||
}
|
||||
|
||||
c := &benchmarkCmd{baseBuilderCmd: b.newBuilderCmd(cmd)}
|
||||
|
||||
cmd.Flags().StringVar(&c.cpuProfileFile, "cpuprofile", "", "path/filename for the CPU profile file")
|
||||
cmd.Flags().StringVar(&c.memProfileFile, "memprofile", "", "path/filename for the memory profile file")
|
||||
cmd.Flags().IntVarP(&c.benchmarkTimes, "count", "n", 13, "number of times to build the site")
|
||||
cmd.Flags().Bool("renderToMemory", false, "render to memory (only useful for benchmark testing)")
|
||||
|
||||
cmd.RunE = c.benchmark
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *benchmarkCmd) benchmark(cmd *cobra.Command, args []string) error {
|
||||
cfgInit := func(c *commandeer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
comm, err := initializeConfig(true, false, &c.hugoBuilderCommon, c, cfgInit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var memProf *os.File
|
||||
if c.memProfileFile != "" {
|
||||
memProf, err = os.Create(c.memProfileFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var cpuProf *os.File
|
||||
if c.cpuProfileFile != "" {
|
||||
cpuProf, err = os.Create(c.cpuProfileFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var memStats runtime.MemStats
|
||||
runtime.ReadMemStats(&memStats)
|
||||
memAllocated := memStats.TotalAlloc
|
||||
mallocs := memStats.Mallocs
|
||||
if cpuProf != nil {
|
||||
pprof.StartCPUProfile(cpuProf)
|
||||
}
|
||||
|
||||
t := time.Now()
|
||||
for i := 0; i < c.benchmarkTimes; i++ {
|
||||
if err = comm.resetAndBuildSites(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
totalTime := time.Since(t)
|
||||
|
||||
if memProf != nil {
|
||||
pprof.WriteHeapProfile(memProf)
|
||||
memProf.Close()
|
||||
}
|
||||
if cpuProf != nil {
|
||||
pprof.StopCPUProfile()
|
||||
cpuProf.Close()
|
||||
}
|
||||
|
||||
runtime.ReadMemStats(&memStats)
|
||||
totalMemAllocated := memStats.TotalAlloc - memAllocated
|
||||
totalMallocs := memStats.Mallocs - mallocs
|
||||
|
||||
jww.FEEDBACK.Println()
|
||||
jww.FEEDBACK.Printf("Average time per operation: %vms\n", int(1000*totalTime.Seconds()/float64(c.benchmarkTimes)))
|
||||
jww.FEEDBACK.Printf("Average memory allocated per operation: %vkB\n", totalMemAllocated/uint64(c.benchmarkTimes)/1024)
|
||||
jww.FEEDBACK.Printf("Average allocations per operation: %v\n", totalMallocs/uint64(c.benchmarkTimes))
|
||||
|
||||
return nil
|
||||
}
|
||||
+104
-40
@@ -1,4 +1,4 @@
|
||||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
// 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.
|
||||
@@ -14,19 +14,33 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/sync/semaphore"
|
||||
|
||||
"github.com/gohugoio/hugo/modules"
|
||||
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/gohugoio/hugo/common/herrors"
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
"github.com/spf13/afero"
|
||||
|
||||
"github.com/bep/debounce"
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
@@ -45,6 +59,8 @@ type commandeerHugoState struct {
|
||||
type commandeer struct {
|
||||
*commandeerHugoState
|
||||
|
||||
logger *loggers.Logger
|
||||
|
||||
// Currently only set when in "fast render mode". But it seems to
|
||||
// be fast enough that we could maybe just add it for all server modes.
|
||||
changeDetector *fileChangeDetector
|
||||
@@ -68,8 +84,47 @@ type commandeer struct {
|
||||
serverPorts []int
|
||||
languagesConfigured bool
|
||||
languages langs.Languages
|
||||
doLiveReload bool
|
||||
fastRenderMode bool
|
||||
showErrorInBrowser bool
|
||||
|
||||
configured bool
|
||||
paused bool
|
||||
|
||||
fullRebuildSem *semaphore.Weighted
|
||||
|
||||
// Any error from the last build.
|
||||
buildErr error
|
||||
}
|
||||
|
||||
func (c *commandeer) errCount() int {
|
||||
return int(c.logger.ErrorCounter.Count())
|
||||
}
|
||||
|
||||
func (c *commandeer) getErrorWithContext() interface{} {
|
||||
errCount := c.errCount()
|
||||
|
||||
if errCount == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
m := make(map[string]interface{})
|
||||
|
||||
m["Error"] = errors.New(removeErrorPrefixFromLog(c.logger.Errors()))
|
||||
m["Version"] = hugo.BuildVersionString()
|
||||
|
||||
fe := herrors.UnwrapErrorWithFileContext(c.buildErr)
|
||||
if fe != nil {
|
||||
m["File"] = fe
|
||||
}
|
||||
|
||||
if c.h.verbose {
|
||||
var b bytes.Buffer
|
||||
herrors.FprintStackTrace(&b, c.buildErr)
|
||||
m["StackTrace"] = b.String()
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func (c *commandeer) Set(key string, value interface{}) {
|
||||
@@ -93,7 +148,7 @@ func newCommandeer(mustHaveConfigFile, running bool, h *hugoBuilderCommon, f fla
|
||||
// The time value used is tested with mass content replacements in a fairly big Hugo site.
|
||||
// It is better to wait for some seconds in those cases rather than get flooded
|
||||
// with rebuilds.
|
||||
rebuildDebouncer, _, _ = debounce.New(4 * time.Second)
|
||||
rebuildDebouncer = debounce.New(4 * time.Second)
|
||||
}
|
||||
|
||||
c := &commandeer{
|
||||
@@ -103,6 +158,9 @@ func newCommandeer(mustHaveConfigFile, running bool, h *hugoBuilderCommon, f fla
|
||||
doWithCommandeer: doWithCommandeer,
|
||||
visitedURLs: types.NewEvictingStringQueue(10),
|
||||
debounce: rebuildDebouncer,
|
||||
fullRebuildSem: semaphore.NewWeighted(1),
|
||||
// This will be replaced later, but we need something to log to before the configuration is read.
|
||||
logger: loggers.NewLogger(jww.LevelError, jww.LevelError, os.Stdout, ioutil.Discard, running),
|
||||
}
|
||||
|
||||
return c, c.loadConfig(mustHaveConfigFile, running)
|
||||
@@ -176,6 +234,11 @@ func (c *commandeer) loadConfig(mustHaveConfigFile, running bool) error {
|
||||
c.DepsCfg = &deps.DepsCfg{}
|
||||
}
|
||||
|
||||
if c.logger != nil {
|
||||
// Truncate the error log if this is a reload.
|
||||
c.logger.Reset()
|
||||
}
|
||||
|
||||
cfg := c.DepsCfg
|
||||
c.configured = false
|
||||
cfg.Running = running
|
||||
@@ -192,6 +255,8 @@ func (c *commandeer) loadConfig(mustHaveConfigFile, running bool) error {
|
||||
sourceFs = c.DepsCfg.Fs.Source
|
||||
}
|
||||
|
||||
environment := c.h.getEnvironment(running)
|
||||
|
||||
doWithConfig := func(cfg config.Provider) error {
|
||||
|
||||
if c.ftch != nil {
|
||||
@@ -199,7 +264,7 @@ func (c *commandeer) loadConfig(mustHaveConfigFile, running bool) error {
|
||||
}
|
||||
|
||||
cfg.Set("workingDir", dir)
|
||||
|
||||
cfg.Set("environment", environment)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -212,8 +277,19 @@ func (c *commandeer) loadConfig(mustHaveConfigFile, running bool) error {
|
||||
return err
|
||||
}
|
||||
|
||||
configPath := c.h.source
|
||||
if configPath == "" {
|
||||
configPath = dir
|
||||
}
|
||||
config, configFiles, err := hugolib.LoadConfig(
|
||||
hugolib.ConfigSourceDescriptor{Fs: sourceFs, Path: c.h.source, WorkingDir: dir, Filename: c.h.cfgFile},
|
||||
hugolib.ConfigSourceDescriptor{
|
||||
Fs: sourceFs,
|
||||
Path: configPath,
|
||||
WorkingDir: dir,
|
||||
Filename: c.h.cfgFile,
|
||||
AbsConfigDir: c.h.getConfigDir(dir),
|
||||
Environ: os.Environ(),
|
||||
Environment: environment},
|
||||
doWithCommandeer,
|
||||
doWithConfig)
|
||||
|
||||
@@ -221,7 +297,7 @@ func (c *commandeer) loadConfig(mustHaveConfigFile, running bool) error {
|
||||
if mustHaveConfigFile {
|
||||
return err
|
||||
}
|
||||
if err != hugolib.ErrNoConfigFile {
|
||||
if err != hugolib.ErrNoConfigFile && !modules.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -234,6 +310,11 @@ func (c *commandeer) loadConfig(mustHaveConfigFile, running bool) error {
|
||||
c.languages = l
|
||||
}
|
||||
|
||||
// Set some commonly used flags
|
||||
c.doLiveReload = running && !c.Cfg.GetBool("disableLiveReload")
|
||||
c.fastRenderMode = c.doLiveReload && !c.Cfg.GetBool("disableFastRender")
|
||||
c.showErrorInBrowser = c.doLiveReload && !c.Cfg.GetBool("disableBrowserError")
|
||||
|
||||
// This is potentially double work, but we need to do this one more time now
|
||||
// that all the languages have been configured.
|
||||
if c.doWithCommandeer != nil {
|
||||
@@ -242,12 +323,13 @@ func (c *commandeer) loadConfig(mustHaveConfigFile, running bool) error {
|
||||
}
|
||||
}
|
||||
|
||||
logger, err := c.createLogger(config)
|
||||
logger, err := c.createLogger(config, running)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg.Logger = logger
|
||||
c.logger = logger
|
||||
|
||||
createMemFs := config.GetBool("renderToMemory")
|
||||
|
||||
@@ -267,10 +349,7 @@ func (c *commandeer) loadConfig(mustHaveConfigFile, running bool) error {
|
||||
fs.Destination = new(afero.MemMapFs)
|
||||
}
|
||||
|
||||
doLiveReload := !c.h.buildWatch && !config.GetBool("disableLiveReload")
|
||||
fastRenderMode := doLiveReload && !config.GetBool("disableFastRender")
|
||||
|
||||
if fastRenderMode {
|
||||
if c.fastRenderMode {
|
||||
// For now, fast render mode only. It should, however, be fast enough
|
||||
// for the full variant, too.
|
||||
changeDetector := &fileChangeDetector{
|
||||
@@ -279,11 +358,19 @@ func (c *commandeer) loadConfig(mustHaveConfigFile, running bool) error {
|
||||
// to make that decision.
|
||||
irrelevantRe: regexp.MustCompile(`\.map$`),
|
||||
}
|
||||
|
||||
changeDetector.PrepareNew()
|
||||
fs.Destination = hugofs.NewHashingFs(fs.Destination, changeDetector)
|
||||
c.changeDetector = changeDetector
|
||||
}
|
||||
|
||||
if c.Cfg.GetBool("logPathWarnings") {
|
||||
fs.Destination = hugofs.NewCreateCountingFs(fs.Destination)
|
||||
}
|
||||
|
||||
// To debug hard-to-find path issues.
|
||||
//fs.Destination = hugofs.NewStacktracerFs(fs.Destination, `fr/fr`)
|
||||
|
||||
err = c.initFs(fs)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -300,37 +387,14 @@ func (c *commandeer) loadConfig(mustHaveConfigFile, running bool) error {
|
||||
return err
|
||||
}
|
||||
|
||||
cacheDir := config.GetString("cacheDir")
|
||||
if cacheDir != "" {
|
||||
if helpers.FilePathSeparator != cacheDir[len(cacheDir)-1:] {
|
||||
cacheDir = cacheDir + helpers.FilePathSeparator
|
||||
}
|
||||
isDir, err := helpers.DirExists(cacheDir, sourceFs)
|
||||
checkErr(cfg.Logger, err)
|
||||
if !isDir {
|
||||
mkdir(cacheDir)
|
||||
}
|
||||
config.Set("cacheDir", cacheDir)
|
||||
} else {
|
||||
config.Set("cacheDir", helpers.GetTempDir("hugo_cache", sourceFs))
|
||||
cacheDir, err := helpers.GetCacheDir(sourceFs, config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config.Set("cacheDir", cacheDir)
|
||||
|
||||
cfg.Logger.INFO.Println("Using config file:", config.ConfigFileUsed())
|
||||
|
||||
themeDir := c.hugo.PathSpec.GetFirstThemeDir()
|
||||
if themeDir != "" {
|
||||
if _, err := sourceFs.Stat(themeDir); os.IsNotExist(err) {
|
||||
return newSystemError("Unable to find theme Directory:", themeDir)
|
||||
}
|
||||
}
|
||||
|
||||
themeVersionMismatch, minVersion := c.isThemeVsHugoVersionMismatch(sourceFs)
|
||||
|
||||
if themeVersionMismatch {
|
||||
cfg.Logger.ERROR.Printf("Current theme does not support Hugo version %s. Minimum version required is %s\n",
|
||||
helpers.CurrentHugoVersion.ReleaseVersion(), minVersion)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
+73
-43
@@ -1,4 +1,4 @@
|
||||
// Copyright 2017 The Hugo Authors. All rights reserved.
|
||||
// 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.
|
||||
@@ -16,12 +16,13 @@ package commands
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/gohugoio/hugo/hugolib/paths"
|
||||
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/spf13/cobra"
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
|
||||
"github.com/spf13/nitro"
|
||||
)
|
||||
|
||||
type commandsBuilder struct {
|
||||
@@ -46,13 +47,14 @@ func (b *commandsBuilder) addAll() *commandsBuilder {
|
||||
newEnvCmd(),
|
||||
newConfigCmd(),
|
||||
newCheckCmd(),
|
||||
b.newBenchmarkCmd(),
|
||||
newDeployCmd(),
|
||||
newConvertCmd(),
|
||||
newNewCmd(),
|
||||
b.newNewCmd(),
|
||||
newListCmd(),
|
||||
newImportCmd(),
|
||||
newGenCmd(),
|
||||
createReleaser(),
|
||||
b.newModCmd(),
|
||||
)
|
||||
|
||||
return b
|
||||
@@ -82,14 +84,14 @@ var _ commandsBuilderGetter = (*baseBuilderCmd)(nil)
|
||||
|
||||
// Used in tests.
|
||||
type commandsBuilderGetter interface {
|
||||
getCmmandsBuilder() *commandsBuilder
|
||||
getCommandsBuilder() *commandsBuilder
|
||||
}
|
||||
type baseBuilderCmd struct {
|
||||
*baseCmd
|
||||
*commandsBuilder
|
||||
}
|
||||
|
||||
func (b *baseBuilderCmd) getCmmandsBuilder() *commandsBuilder {
|
||||
func (b *baseBuilderCmd) getCommandsBuilder() *commandsBuilder {
|
||||
return b.commandsBuilder
|
||||
}
|
||||
|
||||
@@ -162,11 +164,11 @@ Complete documentation is available at http://gohugo.io/.`,
|
||||
})
|
||||
|
||||
cc.cmd.PersistentFlags().StringVar(&cc.cfgFile, "config", "", "config file (default is path/config.yaml|json|toml)")
|
||||
cc.cmd.PersistentFlags().StringVar(&cc.cfgDir, "configDir", "config", "config dir")
|
||||
cc.cmd.PersistentFlags().BoolVar(&cc.quiet, "quiet", false, "build in quiet mode")
|
||||
|
||||
// Set bash-completion
|
||||
validConfigFilenames := []string{"json", "js", "yaml", "yml", "toml", "tml"}
|
||||
_ = cc.cmd.PersistentFlags().SetAnnotation("config", cobra.BashCompFilenameExt, validConfigFilenames)
|
||||
_ = cc.cmd.PersistentFlags().SetAnnotation("config", cobra.BashCompFilenameExt, config.ValidConfigFileExtensions)
|
||||
|
||||
cc.cmd.PersistentFlags().BoolVarP(&cc.verbose, "verbose", "v", false, "verbose output")
|
||||
cc.cmd.PersistentFlags().BoolVarP(&cc.debug, "debug", "", false, "debug output")
|
||||
@@ -188,13 +190,20 @@ Complete documentation is available at http://gohugo.io/.`,
|
||||
}
|
||||
|
||||
type hugoBuilderCommon struct {
|
||||
source string
|
||||
baseURL string
|
||||
source string
|
||||
baseURL string
|
||||
environment string
|
||||
|
||||
buildWatch bool
|
||||
|
||||
gc bool
|
||||
|
||||
// Profile flags (for debugging of performance problems)
|
||||
cpuprofile string
|
||||
memprofile string
|
||||
mutexprofile string
|
||||
traceprofile string
|
||||
|
||||
// TODO(bep) var vs string
|
||||
logging bool
|
||||
verbose bool
|
||||
@@ -203,40 +212,83 @@ type hugoBuilderCommon struct {
|
||||
quiet bool
|
||||
|
||||
cfgFile string
|
||||
cfgDir string
|
||||
logFile string
|
||||
}
|
||||
|
||||
func (cc *hugoBuilderCommon) getConfigDir(baseDir string) string {
|
||||
if cc.cfgDir != "" {
|
||||
return paths.AbsPathify(baseDir, cc.cfgDir)
|
||||
}
|
||||
|
||||
if v, found := os.LookupEnv("HUGO_CONFIGDIR"); found {
|
||||
return paths.AbsPathify(baseDir, v)
|
||||
}
|
||||
|
||||
return paths.AbsPathify(baseDir, "config")
|
||||
}
|
||||
|
||||
func (cc *hugoBuilderCommon) getEnvironment(isServer bool) string {
|
||||
if cc.environment != "" {
|
||||
return cc.environment
|
||||
}
|
||||
|
||||
if v, found := os.LookupEnv("HUGO_ENVIRONMENT"); found {
|
||||
return v
|
||||
}
|
||||
|
||||
if isServer {
|
||||
return hugo.EnvironmentDevelopment
|
||||
}
|
||||
|
||||
return hugo.EnvironmentProduction
|
||||
}
|
||||
|
||||
func (cc *hugoBuilderCommon) handleCommonBuilderFlags(cmd *cobra.Command) {
|
||||
cmd.PersistentFlags().StringVarP(&cc.source, "source", "s", "", "filesystem path to read files relative from")
|
||||
cmd.PersistentFlags().SetAnnotation("source", cobra.BashCompSubdirsInDir, []string{})
|
||||
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")
|
||||
}
|
||||
|
||||
func (cc *hugoBuilderCommon) handleFlags(cmd *cobra.Command) {
|
||||
cc.handleCommonBuilderFlags(cmd)
|
||||
cmd.Flags().Bool("cleanDestinationDir", false, "remove files from destination not found in static directories")
|
||||
cmd.Flags().BoolP("buildDrafts", "D", false, "include content marked as draft")
|
||||
cmd.Flags().BoolP("buildFuture", "F", false, "include content with publishdate in the future")
|
||||
cmd.Flags().BoolP("buildExpired", "E", false, "include expired content")
|
||||
cmd.Flags().StringVarP(&cc.source, "source", "s", "", "filesystem path to read files relative from")
|
||||
cmd.Flags().StringP("contentDir", "c", "", "filesystem path to content directory")
|
||||
cmd.Flags().StringP("layoutDir", "l", "", "filesystem path to layout directory")
|
||||
cmd.Flags().StringP("cacheDir", "", "", "filesystem path to cache directory. Defaults: $TMPDIR/hugo_cache/")
|
||||
cmd.Flags().BoolP("ignoreCache", "", false, "ignores the cache directory")
|
||||
cmd.Flags().StringP("destination", "d", "", "filesystem path to write files to")
|
||||
cmd.Flags().StringP("theme", "t", "", "theme to use (located in /themes/THEMENAME/)")
|
||||
cmd.Flags().StringP("themesDir", "", "", "filesystem path to themes directory")
|
||||
cmd.Flags().Bool("uglyURLs", false, "(deprecated) if true, use /filename.html instead of /filename/")
|
||||
cmd.Flags().Bool("canonifyURLs", false, "(deprecated) if true, all relative URLs will be canonicalized using baseURL")
|
||||
cmd.Flags().StringSliceP("theme", "t", []string{}, "themes to use (located in /themes/THEMENAME/)")
|
||||
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().BoolVar(&nitro.AnalysisOn, "stepAnalysis", false, "display memory and timing of different steps of the program")
|
||||
cmd.Flags().Bool("templateMetrics", false, "display metrics about template executions")
|
||||
cmd.Flags().Bool("templateMetricsHints", false, "calculate some improvement hints when combined with --templateMetrics")
|
||||
cmd.Flags().Bool("pluralizeListTitles", true, "(deprecated) pluralize titles in lists using inflect")
|
||||
cmd.Flags().Bool("preserveTaxonomyNames", false, `(deprecated) preserve taxonomy names as written ("Gérard Depardieu" vs "gerard-depardieu")`)
|
||||
cmd.Flags().BoolP("forceSyncStatic", "", false, "copy all files when static is changed.")
|
||||
cmd.Flags().BoolP("noTimes", "", false, "don't sync modification time of files")
|
||||
cmd.Flags().BoolP("noChmod", "", false, "don't sync permission mode of files")
|
||||
cmd.Flags().BoolP("i18n-warnings", "", false, "print missing translations")
|
||||
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().StringVarP(&cc.mutexprofile, "profile-mutex", "", "", "write Mutex profile to `file`")
|
||||
cmd.Flags().StringVarP(&cc.traceprofile, "trace", "", "", "write trace to `file` (not useful in general)")
|
||||
|
||||
// Hide these for now.
|
||||
cmd.Flags().MarkHidden("profile-cpu")
|
||||
cmd.Flags().MarkHidden("profile-mem")
|
||||
cmd.Flags().MarkHidden("profile-mutex")
|
||||
|
||||
cmd.Flags().StringSlice("disableKinds", []string{}, "disable different kind of pages (home, RSS etc.)")
|
||||
|
||||
cmd.Flags().Bool("minify", false, "minify any supported output format (HTML, XML etc.)")
|
||||
|
||||
// Set bash-completion.
|
||||
// Each flag must first be defined before using the SetAnnotation() call.
|
||||
_ = cmd.Flags().SetAnnotation("source", cobra.BashCompSubdirsInDir, []string{})
|
||||
@@ -245,7 +297,7 @@ func (cc *hugoBuilderCommon) handleFlags(cmd *cobra.Command) {
|
||||
_ = cmd.Flags().SetAnnotation("theme", cobra.BashCompSubdirsInDir, []string{"themes"})
|
||||
}
|
||||
|
||||
func checkErr(logger *jww.Notepad, err error, s ...string) {
|
||||
func checkErr(logger *loggers.Logger, err error, s ...string) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
@@ -258,25 +310,3 @@ func checkErr(logger *jww.Notepad, err error, s ...string) {
|
||||
}
|
||||
logger.ERROR.Println(err)
|
||||
}
|
||||
|
||||
func stopOnErr(logger *jww.Notepad, err error, s ...string) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
defer os.Exit(-1)
|
||||
|
||||
if len(s) == 0 {
|
||||
newMessage := err.Error()
|
||||
// Printing an empty string results in a error with
|
||||
// no message, no bueno.
|
||||
if newMessage != "" {
|
||||
logger.CRITICAL.Println(newMessage)
|
||||
}
|
||||
}
|
||||
for _, message := range s {
|
||||
if message != "" {
|
||||
logger.CRITICAL.Println(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+49
-16
@@ -1,4 +1,4 @@
|
||||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
// 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.
|
||||
@@ -20,6 +20,8 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
@@ -30,7 +32,7 @@ func TestExecute(t *testing.T) {
|
||||
|
||||
assert := require.New(t)
|
||||
|
||||
dir, err := createSimpleTestSite(t)
|
||||
dir, err := createSimpleTestSite(t, testSiteConfig{})
|
||||
assert.NoError(err)
|
||||
|
||||
defer func() {
|
||||
@@ -41,7 +43,7 @@ func TestExecute(t *testing.T) {
|
||||
assert.NoError(resp.Err)
|
||||
result := resp.Result
|
||||
assert.True(len(result.Sites) == 1)
|
||||
assert.True(len(result.Sites[0].RegularPages) == 1)
|
||||
assert.True(len(result.Sites[0].RegularPages()) == 1)
|
||||
}
|
||||
|
||||
func TestCommandsPersistentFlags(t *testing.T) {
|
||||
@@ -56,8 +58,11 @@ func TestCommandsPersistentFlags(t *testing.T) {
|
||||
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",
|
||||
@@ -72,12 +77,14 @@ func TestCommandsPersistentFlags(t *testing.T) {
|
||||
"--port=1366",
|
||||
"--renderToDisk",
|
||||
"--source=mysource",
|
||||
"--uglyURLs"}, func(commands []cmder) {
|
||||
"--path-warnings",
|
||||
}, func(commands []cmder) {
|
||||
var sc *serverCmd
|
||||
for _, command := range commands {
|
||||
if b, ok := command.(commandsBuilderGetter); ok {
|
||||
v := b.getCmmandsBuilder().hugoBuilderCommon
|
||||
v := b.getCommandsBuilder().hugoBuilderCommon
|
||||
assert.Equal("myconfig.toml", v.cfgFile)
|
||||
assert.Equal("myconfigdir", v.cfgDir)
|
||||
assert.Equal("mysource", v.source)
|
||||
assert.Equal("https://example.com/b/", v.baseURL)
|
||||
}
|
||||
@@ -93,21 +100,24 @@ func TestCommandsPersistentFlags(t *testing.T) {
|
||||
assert.True(sc.noHTTPCache)
|
||||
assert.True(sc.renderToDisk)
|
||||
assert.Equal(1366, sc.serverPort)
|
||||
assert.Equal("testing", sc.environment)
|
||||
|
||||
cfg := viper.New()
|
||||
sc.flagsToConfig(cfg)
|
||||
assert.Equal("/tmp/mydestination", cfg.GetString("publishDir"))
|
||||
assert.Equal("mycontent", cfg.GetString("contentDir"))
|
||||
assert.Equal("mylayouts", cfg.GetString("layoutDir"))
|
||||
assert.Equal("mytheme", cfg.GetString("theme"))
|
||||
assert.Equal([]string{"mytheme"}, cfg.GetStringSlice("theme"))
|
||||
assert.Equal("mythemes", cfg.GetString("themesDir"))
|
||||
assert.Equal("https://example.com/b/", cfg.GetString("baseURL"))
|
||||
|
||||
assert.Equal([]string{"page", "home"}, cfg.Get("disableKinds"))
|
||||
|
||||
assert.True(cfg.GetBool("uglyURLs"))
|
||||
assert.True(cfg.GetBool("gc"))
|
||||
|
||||
// The flag is named path-warnings
|
||||
assert.True(cfg.GetBool("logPathWarnings"))
|
||||
|
||||
// The flag is named i18n-warnings
|
||||
assert.True(cfg.GetBool("logI18nWarnings"))
|
||||
|
||||
@@ -136,7 +146,7 @@ func TestCommandsExecute(t *testing.T) {
|
||||
|
||||
assert := require.New(t)
|
||||
|
||||
dir, err := createSimpleTestSite(t)
|
||||
dir, err := createSimpleTestSite(t, testSiteConfig{})
|
||||
assert.NoError(err)
|
||||
|
||||
dirOut, err := ioutil.TempDir("", "hugo-cli-out")
|
||||
@@ -161,7 +171,6 @@ func TestCommandsExecute(t *testing.T) {
|
||||
{nil, []string{sourceFlag}, ""},
|
||||
{nil, []string{sourceFlag, "--renderToMemory"}, ""},
|
||||
{[]string{"config"}, []string{sourceFlag}, ""},
|
||||
{[]string{"benchmark"}, []string{sourceFlag, "-n=1"}, ""},
|
||||
{[]string{"convert", "toTOML"}, []string{sourceFlag, "-o=" + filepath.Join(dirOut, "toml")}, ""},
|
||||
{[]string{"convert", "toYAML"}, []string{sourceFlag, "-o=" + filepath.Join(dirOut, "yaml")}, ""},
|
||||
{[]string{"convert", "toJSON"}, []string{sourceFlag, "-o=" + filepath.Join(dirOut, "json")}, ""},
|
||||
@@ -180,8 +189,8 @@ func TestCommandsExecute(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
||||
hugoCmd := newCommandsBuilder().addAll().build().getCommand()
|
||||
b := newCommandsBuilder().addAll().build()
|
||||
hugoCmd := b.getCommand()
|
||||
test.flags = append(test.flags, "--quiet")
|
||||
hugoCmd.SetArgs(append(test.commands, test.flags...))
|
||||
|
||||
@@ -197,25 +206,48 @@ func TestCommandsExecute(t *testing.T) {
|
||||
assert.NoError(err, fmt.Sprintf("%v", test.commands))
|
||||
}
|
||||
|
||||
// Assert that we have not left any development debug artifacts in
|
||||
// the code.
|
||||
if b.c != nil {
|
||||
_, ok := b.c.destinationFs.(types.DevMarker)
|
||||
assert.False(ok)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func createSimpleTestSite(t *testing.T) (string, error) {
|
||||
type testSiteConfig struct {
|
||||
configTOML string
|
||||
contentDir string
|
||||
}
|
||||
|
||||
func createSimpleTestSite(t *testing.T, cfg testSiteConfig) (string, error) {
|
||||
d, e := ioutil.TempDir("", "hugo-cli")
|
||||
if e != nil {
|
||||
return "", e
|
||||
}
|
||||
|
||||
// Just the basic. These are for CLI tests, not site testing.
|
||||
writeFile(t, filepath.Join(d, "config.toml"), `
|
||||
cfgStr := `
|
||||
|
||||
baseURL = "https://example.org"
|
||||
title = "Hugo Commands"
|
||||
|
||||
`)
|
||||
`
|
||||
|
||||
writeFile(t, filepath.Join(d, "content", "p1.md"), `
|
||||
contentDir := "content"
|
||||
|
||||
if cfg.configTOML != "" {
|
||||
cfgStr = cfg.configTOML
|
||||
}
|
||||
if cfg.contentDir != "" {
|
||||
contentDir = cfg.contentDir
|
||||
}
|
||||
|
||||
// Just the basic. These are for CLI tests, not site testing.
|
||||
writeFile(t, filepath.Join(d, "config.toml"), cfgStr)
|
||||
|
||||
writeFile(t, filepath.Join(d, contentDir, "p1.md"), `
|
||||
---
|
||||
title: "P1"
|
||||
weight: 1
|
||||
@@ -234,6 +266,7 @@ Single: {{ .Title }}
|
||||
writeFile(t, filepath.Join(d, "layouts", "_default", "list.html"), `
|
||||
|
||||
List: {{ .Title }}
|
||||
Environment: {{ hugo.Environment }}
|
||||
|
||||
`)
|
||||
|
||||
|
||||
+103
-47
@@ -1,4 +1,4 @@
|
||||
// Copyright 2015 The Hugo Authors. All rights reserved.
|
||||
// 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.
|
||||
@@ -14,17 +14,28 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
src "github.com/gohugoio/hugo/source"
|
||||
"github.com/gohugoio/hugo/resources/page"
|
||||
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
|
||||
"github.com/gohugoio/hugo/parser"
|
||||
"github.com/gohugoio/hugo/parser/metadecoders"
|
||||
"github.com/gohugoio/hugo/parser/pageparser"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gohugoio/hugo/parser"
|
||||
"github.com/spf13/cast"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -60,7 +71,7 @@ See convert's subcommands toJSON, toTOML and toYAML for more information.`,
|
||||
Long: `toJSON converts all front matter in the content directory
|
||||
to use JSON for the front matter.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return cc.convertContents(rune([]byte(parser.JSONLead)[0]))
|
||||
return cc.convertContents(metadecoders.JSON)
|
||||
},
|
||||
},
|
||||
&cobra.Command{
|
||||
@@ -69,7 +80,7 @@ to use JSON for the front matter.`,
|
||||
Long: `toTOML converts all front matter in the content directory
|
||||
to use TOML for the front matter.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return cc.convertContents(rune([]byte(parser.TOMLLead)[0]))
|
||||
return cc.convertContents(metadecoders.TOML)
|
||||
},
|
||||
},
|
||||
&cobra.Command{
|
||||
@@ -78,7 +89,7 @@ to use TOML for the front matter.`,
|
||||
Long: `toYAML converts all front matter in the content directory
|
||||
to use YAML for the front matter.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return cc.convertContents(rune([]byte(parser.YAMLLead)[0]))
|
||||
return cc.convertContents(metadecoders.YAML)
|
||||
},
|
||||
},
|
||||
)
|
||||
@@ -91,7 +102,7 @@ to use YAML for the front matter.`,
|
||||
return cc
|
||||
}
|
||||
|
||||
func (cc *convertCmd) convertContents(mark rune) error {
|
||||
func (cc *convertCmd) convertContents(format metadecoders.Format) error {
|
||||
if cc.outputDir == "" && !cc.unsafe {
|
||||
return newUserError("Unsafe operation not allowed, use --unsafe or set a different output path")
|
||||
}
|
||||
@@ -101,6 +112,8 @@ func (cc *convertCmd) convertContents(mark rune) error {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Cfg.Set("buildDrafts", true)
|
||||
|
||||
h, err := hugolib.NewHugoSites(*c.DepsCfg)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -112,83 +125,126 @@ func (cc *convertCmd) convertContents(mark rune) error {
|
||||
|
||||
site := h.Sites[0]
|
||||
|
||||
site.Log.FEEDBACK.Println("processing", len(site.AllPages), "content files")
|
||||
for _, p := range site.AllPages {
|
||||
if err := cc.convertAndSavePage(p, site, mark); err != nil {
|
||||
site.Log.FEEDBACK.Println("processing", len(site.AllPages()), "content files")
|
||||
for _, p := range site.AllPages() {
|
||||
if err := cc.convertAndSavePage(p, site, format); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cc *convertCmd) convertAndSavePage(p *hugolib.Page, site *hugolib.Site, mark rune) error {
|
||||
func (cc *convertCmd) convertAndSavePage(p page.Page, site *hugolib.Site, targetFormat metadecoders.Format) error {
|
||||
// The resources are not in .Site.AllPages.
|
||||
for _, r := range p.Resources.ByType("page") {
|
||||
if err := cc.convertAndSavePage(r.(*hugolib.Page), site, mark); err != nil {
|
||||
for _, r := range p.Resources().ByType("page") {
|
||||
if err := cc.convertAndSavePage(r.(page.Page), site, targetFormat); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if p.Filename() == "" {
|
||||
if p.File().IsZero() {
|
||||
// No content file.
|
||||
return nil
|
||||
}
|
||||
|
||||
site.Log.INFO.Println("Attempting to convert", p.LogicalName())
|
||||
newPage, err := site.NewPage(p.LogicalName())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
errMsg := fmt.Errorf("Error processing file %q", p.Path())
|
||||
|
||||
f, _ := p.File.(src.ReadableFile)
|
||||
file, err := f.Open()
|
||||
site.Log.INFO.Println("Attempting to convert", p.File().Filename())
|
||||
|
||||
f := p.File()
|
||||
file, err := f.FileInfo().Meta().Open()
|
||||
if err != nil {
|
||||
site.Log.ERROR.Println("Error reading file:", p.Path())
|
||||
site.Log.ERROR.Println(errMsg)
|
||||
file.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
psr, err := parser.ReadFrom(file)
|
||||
pf, err := parseContentFile(file)
|
||||
if err != nil {
|
||||
site.Log.ERROR.Println("Error processing file:", p.Path())
|
||||
site.Log.ERROR.Println(errMsg)
|
||||
file.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
file.Close()
|
||||
|
||||
metadata, err := psr.Metadata()
|
||||
// better handling of dates in formats that don't have support for them
|
||||
if pf.frontMatterFormat == metadecoders.JSON || pf.frontMatterFormat == metadecoders.YAML || pf.frontMatterFormat == metadecoders.TOML {
|
||||
for k, v := range pf.frontMatter {
|
||||
switch vv := v.(type) {
|
||||
case time.Time:
|
||||
pf.frontMatter[k] = vv.Format(time.RFC3339)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var newContent bytes.Buffer
|
||||
err = parser.InterfaceToFrontMatter(pf.frontMatter, targetFormat, &newContent)
|
||||
if err != nil {
|
||||
site.Log.ERROR.Println("Error processing file:", p.Path())
|
||||
site.Log.ERROR.Println(errMsg)
|
||||
return err
|
||||
}
|
||||
|
||||
// better handling of dates in formats that don't have support for them
|
||||
if mark == parser.FormatToLeadRune("json") || mark == parser.FormatToLeadRune("yaml") || mark == parser.FormatToLeadRune("toml") {
|
||||
newMetadata := cast.ToStringMap(metadata)
|
||||
for k, v := range newMetadata {
|
||||
switch vv := v.(type) {
|
||||
case time.Time:
|
||||
newMetadata[k] = vv.Format(time.RFC3339)
|
||||
}
|
||||
}
|
||||
metadata = newMetadata
|
||||
}
|
||||
newContent.Write(pf.content)
|
||||
|
||||
newPage.SetSourceContent(psr.Content())
|
||||
if err = newPage.SetSourceMetaData(metadata, mark); err != nil {
|
||||
site.Log.ERROR.Printf("Failed to set source metadata for file %q: %s. For more info see For more info see https://github.com/gohugoio/hugo/issues/2458", newPage.FullFilePath(), err)
|
||||
return nil
|
||||
}
|
||||
newFilename := p.File().Filename()
|
||||
|
||||
newFilename := p.Filename()
|
||||
if cc.outputDir != "" {
|
||||
newFilename = filepath.Join(cc.outputDir, p.Dir(), newPage.LogicalName())
|
||||
contentDir := strings.TrimSuffix(newFilename, p.Path())
|
||||
contentDir = filepath.Base(contentDir)
|
||||
|
||||
newFilename = filepath.Join(cc.outputDir, contentDir, p.Path())
|
||||
}
|
||||
|
||||
if err = newPage.SaveSourceAs(newFilename); err != nil {
|
||||
return fmt.Errorf("Failed to save file %q: %s", newFilename, err)
|
||||
fs := hugofs.Os
|
||||
if err := helpers.WriteToDisk(newFilename, &newContent, fs); err != nil {
|
||||
return errors.Wrapf(err, "Failed to save file %q:", newFilename)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type parsedFile struct {
|
||||
frontMatterFormat metadecoders.Format
|
||||
frontMatterSource []byte
|
||||
frontMatter map[string]interface{}
|
||||
|
||||
// Everything after Front Matter
|
||||
content []byte
|
||||
}
|
||||
|
||||
func parseContentFile(r io.Reader) (parsedFile, error) {
|
||||
var pf parsedFile
|
||||
|
||||
psr, err := pageparser.Parse(r, pageparser.Config{})
|
||||
if err != nil {
|
||||
return pf, err
|
||||
}
|
||||
|
||||
iter := psr.Iterator()
|
||||
|
||||
walkFn := func(item pageparser.Item) bool {
|
||||
if pf.frontMatterSource != nil {
|
||||
// The rest is content.
|
||||
pf.content = psr.Input()[item.Pos:]
|
||||
// Done
|
||||
return false
|
||||
} else if item.IsFrontMatter() {
|
||||
pf.frontMatterFormat = metadecoders.FormatFromFrontMatterType(item.Type)
|
||||
pf.frontMatterSource = item.Val
|
||||
}
|
||||
return true
|
||||
|
||||
}
|
||||
|
||||
iter.PeekWalk(walkFn)
|
||||
|
||||
metadata, err := metadecoders.Default.UnmarshalToMap(pf.frontMatterSource, pf.frontMatterFormat)
|
||||
if err != nil {
|
||||
return pf, err
|
||||
}
|
||||
pf.frontMatter = metadata
|
||||
|
||||
return pf, nil
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
// 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.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gohugoio/hugo/deploy"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var _ cmder = (*deployCmd)(nil)
|
||||
|
||||
// deployCmd supports deploying sites to Cloud providers.
|
||||
type deployCmd struct {
|
||||
hugoBuilderCommon
|
||||
*baseCmd
|
||||
}
|
||||
|
||||
// TODO: In addition to the "deploy" command, consider adding a "--deploy"
|
||||
// flag for the default command; this would build the site and then deploy it.
|
||||
// It's not obvious how to do this; would all of the deploy-specific flags
|
||||
// have to exist at the top level as well?
|
||||
|
||||
// TODO: The output files change every time "hugo" is executed, it looks
|
||||
// like because of map order randomization. This means that you can
|
||||
// run "hugo && hugo deploy" again and again and upload new stuff every time. Is
|
||||
// this intended?
|
||||
|
||||
func newDeployCmd() *deployCmd {
|
||||
cc := &deployCmd{}
|
||||
|
||||
cc.baseCmd = newBaseCmd(&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 {
|
||||
cfgInit := func(c *commandeer) error {
|
||||
return nil
|
||||
}
|
||||
comm, err := initializeConfig(true, false, &cc.hugoBuilderCommon, cc, cfgInit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
deployer, err := deploy.New(comm.Cfg, comm.hugo.PathSpec.PublishFs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return deployer.Deploy(context.Background())
|
||||
},
|
||||
})
|
||||
|
||||
cc.cmd.Flags().String("target", "", "target deployment from deployments section in config file; defaults to the first one")
|
||||
cc.cmd.Flags().Bool("confirm", false, "ask for confirmation before making changes to the target")
|
||||
cc.cmd.Flags().Bool("dryRun", false, "dry run")
|
||||
cc.cmd.Flags().Bool("force", false, "force upload of all files")
|
||||
cc.cmd.Flags().Bool("invalidateCDN", true, "invalidate the CDN cache via the cloudFrontDistributionID listed in the deployment target")
|
||||
cc.cmd.Flags().Int("maxDeletes", 256, "maximum # of files to delete, or -1 to disable")
|
||||
|
||||
return cc
|
||||
}
|
||||
+2
-1
@@ -17,6 +17,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -45,7 +46,7 @@ in the "man" directory under the current directory.`,
|
||||
header := &doc.GenManHeader{
|
||||
Section: "1",
|
||||
Manual: "Hugo Manual",
|
||||
Source: fmt.Sprintf("Hugo %s", helpers.CurrentHugoVersion),
|
||||
Source: fmt.Sprintf("Hugo %s", hugo.CurrentVersion),
|
||||
}
|
||||
if !strings.HasSuffix(cc.genmandir, helpers.FilePathSeparator) {
|
||||
cc.genmandir += helpers.FilePathSeparator
|
||||
|
||||
@@ -23,6 +23,13 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
ansiEsc = "\u001B"
|
||||
clearLine = "\r\033[K"
|
||||
hideCursor = ansiEsc + "[?25l"
|
||||
showCursor = ansiEsc + "[?25h"
|
||||
)
|
||||
|
||||
type flagsToConfigHandler interface {
|
||||
flagsToConfig(cfg config.Provider)
|
||||
}
|
||||
|
||||
+617
-419
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,52 @@
|
||||
// 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.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Issue #5662
|
||||
func TestHugoWithContentDirOverride(t *testing.T) {
|
||||
assert := require.New(t)
|
||||
|
||||
hugoCmd := newCommandsBuilder().addAll().build()
|
||||
cmd := hugoCmd.getCommand()
|
||||
|
||||
contentDir := "contentOverride"
|
||||
|
||||
cfgStr := `
|
||||
|
||||
baseURL = "https://example.org"
|
||||
title = "Hugo Commands"
|
||||
|
||||
contentDir = "thisdoesnotexist"
|
||||
|
||||
`
|
||||
dir, err := createSimpleTestSite(t, testSiteConfig{configTOML: cfgStr, contentDir: contentDir})
|
||||
assert.NoError(err)
|
||||
|
||||
defer func() {
|
||||
os.RemoveAll(dir)
|
||||
}()
|
||||
|
||||
cmd.SetArgs([]string{"-s=" + dir, "-c=" + contentDir})
|
||||
|
||||
_, err = cmd.ExecuteC()
|
||||
assert.NoError(err)
|
||||
|
||||
}
|
||||
+80
-98
@@ -1,4 +1,4 @@
|
||||
// Copyright 2016 The Hugo Authors. All rights reserved.
|
||||
// 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.
|
||||
@@ -16,7 +16,7 @@ package commands
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -24,6 +24,11 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/gohugoio/hugo/common/hugio"
|
||||
|
||||
"github.com/gohugoio/hugo/parser/metadecoders"
|
||||
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
@@ -73,23 +78,23 @@ Import from Jekyll requires two paths, e.g. ` + "`hugo import jekyll jekyll_root
|
||||
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`.")
|
||||
return newUserError(`import from jekyll requires two paths, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`.")
|
||||
}
|
||||
|
||||
jekyllRoot, err := filepath.Abs(filepath.Clean(args[0]))
|
||||
if err != nil {
|
||||
return newUserError("Path error:", args[0])
|
||||
return newUserError("path error:", args[0])
|
||||
}
|
||||
|
||||
targetDir, err := filepath.Abs(filepath.Clean(args[1]))
|
||||
if err != nil {
|
||||
return newUserError("Path error:", args[1])
|
||||
return newUserError("path error:", args[1])
|
||||
}
|
||||
|
||||
jww.INFO.Println("Import Jekyll from:", jekyllRoot, "to:", targetDir)
|
||||
|
||||
if strings.HasPrefix(filepath.Dir(targetDir), jekyllRoot) {
|
||||
return newUserError("Target path should not be inside the Jekyll root, aborting.")
|
||||
return newUserError("abort: target path should not be inside the Jekyll root")
|
||||
}
|
||||
|
||||
forceImport, _ := cmd.Flags().GetBool("force")
|
||||
@@ -97,7 +102,7 @@ func (i *importCmd) importFromJekyll(cmd *cobra.Command, args []string) error {
|
||||
fs := afero.NewOsFs()
|
||||
jekyllPostDirs, hasAnyPost := i.getJekyllDirInfo(fs, jekyllRoot)
|
||||
if !hasAnyPost {
|
||||
return errors.New("Your Jekyll root contains neither posts nor drafts, aborting.")
|
||||
return errors.New("abort: jekyll root contains neither posts nor drafts")
|
||||
}
|
||||
|
||||
site, err := i.createSiteFromJekyll(jekyllRoot, targetDir, jekyllPostDirs, forceImport)
|
||||
@@ -109,7 +114,7 @@ func (i *importCmd) importFromJekyll(cmd *cobra.Command, args []string) error {
|
||||
jww.FEEDBACK.Println("Importing...")
|
||||
|
||||
fileCount := 0
|
||||
callback := func(path string, fi os.FileInfo, err error) error {
|
||||
callback := func(path string, fi hugofs.FileMetaInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -120,7 +125,7 @@ func (i *importCmd) importFromJekyll(cmd *cobra.Command, args []string) error {
|
||||
|
||||
relPath, err := filepath.Rel(jekyllRoot, path)
|
||||
if err != nil {
|
||||
return newUserError("Get rel path error:", path)
|
||||
return newUserError("get rel path error:", path)
|
||||
}
|
||||
|
||||
relPath = filepath.ToSlash(relPath)
|
||||
@@ -204,13 +209,13 @@ func (i *importCmd) createSiteFromJekyll(jekyllRoot, targetDir string, jekyllPos
|
||||
fs := s.Fs.Source
|
||||
if exists, _ := helpers.Exists(targetDir, fs); exists {
|
||||
if isDir, _ := helpers.IsDir(targetDir, fs); !isDir {
|
||||
return nil, errors.New("Target path \"" + targetDir + "\" already exists but not a directory")
|
||||
return nil, errors.New("target path \"" + targetDir + "\" exists but is not a directory")
|
||||
}
|
||||
|
||||
isEmpty, _ := helpers.IsEmpty(targetDir, fs)
|
||||
|
||||
if !isEmpty && !force {
|
||||
return nil, errors.New("Target path \"" + targetDir + "\" already exists and is not empty")
|
||||
return nil, errors.New("target path \"" + targetDir + "\" exists and is not empty")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,7 +258,7 @@ func (i *importCmd) loadJekyllConfig(fs afero.Fs, jekyllRoot string) map[string]
|
||||
return nil
|
||||
}
|
||||
|
||||
c, err := parser.HandleYAMLMetaData(b)
|
||||
c, err := metadecoders.Default.UnmarshalToMap(b, metadecoders.YAML)
|
||||
|
||||
if err != nil {
|
||||
return nil
|
||||
@@ -262,7 +267,7 @@ func (i *importCmd) loadJekyllConfig(fs afero.Fs, jekyllRoot string) map[string]
|
||||
return c
|
||||
}
|
||||
|
||||
func (i *importCmd) createConfigFromJekyll(fs afero.Fs, inpath string, kind string, jekyllConfig map[string]interface{}) (err error) {
|
||||
func (i *importCmd) createConfigFromJekyll(fs afero.Fs, inpath string, kind metadecoders.Format, jekyllConfig map[string]interface{}) (err error) {
|
||||
title := "My New Hugo Site"
|
||||
baseURL := "http://example.org/"
|
||||
|
||||
@@ -288,77 +293,20 @@ func (i *importCmd) createConfigFromJekyll(fs afero.Fs, inpath string, kind stri
|
||||
"languageCode": "en-us",
|
||||
"disablePathToLower": true,
|
||||
}
|
||||
kind = parser.FormatSanitize(kind)
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = parser.InterfaceToConfig(in, parser.FormatToLeadRune(kind), &buf)
|
||||
err = parser.InterfaceToConfig(in, kind, &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return helpers.WriteToDisk(filepath.Join(inpath, "config."+kind), &buf, fs)
|
||||
}
|
||||
|
||||
func copyFile(source string, dest string) error {
|
||||
sf, err := os.Open(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer sf.Close()
|
||||
df, err := os.Create(dest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer df.Close()
|
||||
_, err = io.Copy(df, sf)
|
||||
if err == nil {
|
||||
si, err := os.Stat(source)
|
||||
if err != nil {
|
||||
err = os.Chmod(dest, si.Mode())
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyDir(source string, dest string) error {
|
||||
fi, err := os.Stat(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !fi.IsDir() {
|
||||
return errors.New(source + " is not a directory")
|
||||
}
|
||||
err = os.MkdirAll(dest, fi.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
entries, err := ioutil.ReadDir(source)
|
||||
for _, entry := range entries {
|
||||
sfp := filepath.Join(source, entry.Name())
|
||||
dfp := filepath.Join(dest, entry.Name())
|
||||
if entry.IsDir() {
|
||||
err = copyDir(sfp, dfp)
|
||||
if err != nil {
|
||||
jww.ERROR.Println(err)
|
||||
}
|
||||
} else {
|
||||
err = copyFile(sfp, dfp)
|
||||
if err != nil {
|
||||
jww.ERROR.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
return helpers.WriteToDisk(filepath.Join(inpath, "config."+string(kind)), &buf, fs)
|
||||
}
|
||||
|
||||
func (i *importCmd) copyJekyllFilesAndFolders(jekyllRoot, dest string, jekyllPostDirs map[string]bool) (err error) {
|
||||
fi, err := os.Stat(jekyllRoot)
|
||||
fs := hugofs.Os
|
||||
|
||||
fi, err := fs.Stat(jekyllRoot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -370,13 +318,17 @@ func (i *importCmd) copyJekyllFilesAndFolders(jekyllRoot, dest string, jekyllPos
|
||||
return err
|
||||
}
|
||||
entries, err := ioutil.ReadDir(jekyllRoot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
sfp := filepath.Join(jekyllRoot, entry.Name())
|
||||
dfp := filepath.Join(dest, entry.Name())
|
||||
if entry.IsDir() {
|
||||
if entry.Name()[0] != '_' && entry.Name()[0] != '.' {
|
||||
if _, ok := jekyllPostDirs[entry.Name()]; !ok {
|
||||
err = copyDir(sfp, dfp)
|
||||
err = hugio.CopyDir(fs, sfp, dfp, nil)
|
||||
if err != nil {
|
||||
jww.ERROR.Println(err)
|
||||
}
|
||||
@@ -395,7 +347,7 @@ func (i *importCmd) copyJekyllFilesAndFolders(jekyllRoot, dest string, jekyllPos
|
||||
}
|
||||
|
||||
if !isExcept && entry.Name()[0] != '.' && entry.Name()[0] != '_' {
|
||||
err = copyFile(sfp, dfp)
|
||||
err = hugio.CopyFile(fs, sfp, dfp)
|
||||
if err != nil {
|
||||
jww.ERROR.Println(err)
|
||||
}
|
||||
@@ -445,39 +397,25 @@ func convertJekyllPost(s *hugolib.Site, path, relPath, targetDir string, draft b
|
||||
return err
|
||||
}
|
||||
|
||||
psr, err := parser.ReadFrom(bytes.NewReader(contentBytes))
|
||||
pf, err := parseContentFile(bytes.NewReader(contentBytes))
|
||||
if err != nil {
|
||||
jww.ERROR.Println("Parse file error:", path)
|
||||
return err
|
||||
}
|
||||
|
||||
metadata, err := psr.Metadata()
|
||||
if err != nil {
|
||||
jww.ERROR.Println("Processing file error:", path)
|
||||
return err
|
||||
}
|
||||
|
||||
newmetadata, err := convertJekyllMetaData(metadata, postName, postDate, draft)
|
||||
newmetadata, err := convertJekyllMetaData(pf.frontMatter, postName, postDate, draft)
|
||||
if err != nil {
|
||||
jww.ERROR.Println("Convert metadata error:", path)
|
||||
return err
|
||||
}
|
||||
|
||||
jww.TRACE.Println(newmetadata)
|
||||
content := convertJekyllContent(newmetadata, string(psr.Content()))
|
||||
content := convertJekyllContent(newmetadata, string(pf.content))
|
||||
|
||||
page, err := s.NewPage(filename)
|
||||
if err != nil {
|
||||
jww.ERROR.Println("New page error", filename)
|
||||
return err
|
||||
fs := hugofs.Os
|
||||
if err := helpers.WriteToDisk(targetFile, strings.NewReader(content), fs); err != nil {
|
||||
return fmt.Errorf("failed to save file %q: %s", filename, err)
|
||||
}
|
||||
|
||||
page.SetSourceContent([]byte(content))
|
||||
page.SetSourceMetaData(newmetadata, parser.FormatToLeadRune("yaml"))
|
||||
page.SaveSourceAs(targetFile)
|
||||
|
||||
jww.TRACE.Println("Target file:", targetFile)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -557,7 +495,6 @@ func convertJekyllContent(m interface{}, content string) string {
|
||||
}{
|
||||
{regexp.MustCompile("(?i)<!-- more -->"), "<!--more-->"},
|
||||
{regexp.MustCompile(`\{%\s*raw\s*%\}\s*(.*?)\s*\{%\s*endraw\s*%\}`), "$1"},
|
||||
{regexp.MustCompile(`{%\s*highlight\s*(.*?)\s*%}`), "{{< highlight $1 >}}"},
|
||||
{regexp.MustCompile(`{%\s*endhighlight\s*%}`), "{{< / highlight >}}"},
|
||||
}
|
||||
|
||||
@@ -571,6 +508,7 @@ func convertJekyllContent(m interface{}, content string) string {
|
||||
}{
|
||||
// Octopress image tag: http://octopress.org/docs/plugins/image-tag/
|
||||
{regexp.MustCompile(`{%\s+img\s*(.*?)\s*%}`), replaceImageTag},
|
||||
{regexp.MustCompile(`{%\s*highlight\s*(.*?)\s*%}`), replaceHighlightTag},
|
||||
}
|
||||
|
||||
for _, replace := range replaceListFunc {
|
||||
@@ -580,6 +518,50 @@ func convertJekyllContent(m interface{}, content string) string {
|
||||
return content
|
||||
}
|
||||
|
||||
func replaceHighlightTag(match string) string {
|
||||
r := regexp.MustCompile(`{%\s*highlight\s*(.*?)\s*%}`)
|
||||
parts := r.FindStringSubmatch(match)
|
||||
lastQuote := rune(0)
|
||||
f := func(c rune) bool {
|
||||
switch {
|
||||
case c == lastQuote:
|
||||
lastQuote = rune(0)
|
||||
return false
|
||||
case lastQuote != rune(0):
|
||||
return false
|
||||
case unicode.In(c, unicode.Quotation_Mark):
|
||||
lastQuote = c
|
||||
return false
|
||||
default:
|
||||
return unicode.IsSpace(c)
|
||||
}
|
||||
}
|
||||
// splitting string by space but considering quoted section
|
||||
items := strings.FieldsFunc(parts[1], f)
|
||||
|
||||
result := bytes.NewBufferString("{{< highlight ")
|
||||
result.WriteString(items[0]) // language
|
||||
options := items[1:]
|
||||
for i, opt := range options {
|
||||
opt = strings.Replace(opt, "\"", "", -1)
|
||||
if opt == "linenos" {
|
||||
opt = "linenos=table"
|
||||
}
|
||||
if i == 0 {
|
||||
opt = " \"" + opt
|
||||
}
|
||||
if i < len(options)-1 {
|
||||
opt += ","
|
||||
} else if i == len(options)-1 {
|
||||
opt += "\""
|
||||
}
|
||||
result.WriteString(opt)
|
||||
}
|
||||
|
||||
result.WriteString(" >}}")
|
||||
return result.String()
|
||||
}
|
||||
|
||||
func replaceImageTag(match string) string {
|
||||
r := regexp.MustCompile(`{%\s+img\s*(\p{L}*)\s+([\S]*/[\S]+)\s+(\d*)\s*(\d*)\s*(.*?)\s*%}`)
|
||||
result := bytes.NewBufferString("{{< figure ")
|
||||
|
||||
@@ -97,6 +97,9 @@ func TestConvertJekyllContent(t *testing.T) {
|
||||
{map[interface{}]interface{}{},
|
||||
"{% highlight go %}\nvar s int\n{% endhighlight %}",
|
||||
"{{< 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 >}}"},
|
||||
|
||||
// Octopress image tag
|
||||
{map[interface{}]interface{}{},
|
||||
|
||||
@@ -36,14 +36,17 @@ This is primarily to ensure that Hugo can watch enough files on some OSs`,
|
||||
var rLimit syscall.Rlimit
|
||||
err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit)
|
||||
if err != nil {
|
||||
return newSystemError("Error Getting Rlimit ", err)
|
||||
return newSystemError("Error Getting rlimit ", err)
|
||||
}
|
||||
|
||||
jww.FEEDBACK.Println("Current rLimit:", rLimit)
|
||||
|
||||
if rLimit.Cur >= newRlimit {
|
||||
return nil
|
||||
}
|
||||
|
||||
jww.FEEDBACK.Println("Attempting to increase limit")
|
||||
rLimit.Max = 999999
|
||||
rLimit.Cur = 999999
|
||||
rLimit.Cur = newRlimit
|
||||
err = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit)
|
||||
if err != nil {
|
||||
return newSystemError("Error Setting rLimit ", err)
|
||||
@@ -61,18 +64,21 @@ This is primarily to ensure that Hugo can watch enough files on some OSs`,
|
||||
return &limitCmd{baseCmd: newBaseCmd(ccmd)}
|
||||
}
|
||||
|
||||
const newRlimit = 10240
|
||||
|
||||
func tweakLimit() {
|
||||
var rLimit syscall.Rlimit
|
||||
err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit)
|
||||
if err != nil {
|
||||
jww.ERROR.Println("Unable to obtain rLimit", err)
|
||||
jww.WARN.Println("Unable to get rlimit:", err)
|
||||
return
|
||||
}
|
||||
if rLimit.Cur < rLimit.Max {
|
||||
rLimit.Max = 64000
|
||||
rLimit.Cur = 64000
|
||||
if rLimit.Cur < newRlimit {
|
||||
rLimit.Cur = newRlimit
|
||||
err = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit)
|
||||
if err != nil {
|
||||
jww.WARN.Println("Unable to increase number of open files limit", err)
|
||||
// This may not succeed, see https://github.com/golang/go/issues/30401
|
||||
jww.INFO.Println("Unable to increase number of open files limit:", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+112
-60
@@ -1,4 +1,4 @@
|
||||
// Copyright 2015 The Hugo Authors. All rights reserved.
|
||||
// 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.
|
||||
@@ -14,9 +14,14 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"encoding/csv"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
"github.com/gohugoio/hugo/resources/resource"
|
||||
"github.com/spf13/cobra"
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
)
|
||||
@@ -28,6 +33,32 @@ type listCmd struct {
|
||||
*baseCmd
|
||||
}
|
||||
|
||||
func (lc *listCmd) buildSites(config map[string]interface{}) (*hugolib.HugoSites, error) {
|
||||
cfgInit := func(c *commandeer) error {
|
||||
for key, value := range config {
|
||||
c.Set(key, value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
c, err := initializeConfig(true, false, &lc.hugoBuilderCommon, lc, cfgInit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sites, err := hugolib.NewHugoSites(*c.DepsCfg)
|
||||
|
||||
if err != nil {
|
||||
return nil, newSystemError("Error creating sites", err)
|
||||
}
|
||||
|
||||
if err := sites.Build(hugolib.BuildCfg{SkipRender: true}); err != nil {
|
||||
return nil, newSystemError("Error Processing Source Content", err)
|
||||
}
|
||||
|
||||
return sites, nil
|
||||
}
|
||||
|
||||
func newListCmd() *listCmd {
|
||||
cc := &listCmd{}
|
||||
|
||||
@@ -46,106 +77,127 @@ List requires a subcommand, e.g. ` + "`hugo list drafts`.",
|
||||
Short: "List all drafts",
|
||||
Long: `List all of the drafts in your content directory.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfgInit := func(c *commandeer) error {
|
||||
c.Set("buildDrafts", true)
|
||||
return nil
|
||||
}
|
||||
c, err := initializeConfig(true, false, &cc.hugoBuilderCommon, cc, cfgInit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sites, err := hugolib.NewHugoSites(*c.DepsCfg)
|
||||
sites, err := cc.buildSites(map[string]interface{}{"buildDrafts": true})
|
||||
|
||||
if err != nil {
|
||||
return newSystemError("Error creating sites", err)
|
||||
}
|
||||
|
||||
if err := sites.Build(hugolib.BuildCfg{SkipRender: true}); err != nil {
|
||||
return newSystemError("Error Processing Source Content", err)
|
||||
return newSystemError("Error building sites", err)
|
||||
}
|
||||
|
||||
for _, p := range sites.Pages() {
|
||||
if p.IsDraft() {
|
||||
jww.FEEDBACK.Println(filepath.Join(p.File.Dir(), p.File.LogicalName()))
|
||||
if p.Draft() {
|
||||
jww.FEEDBACK.Println(strings.TrimPrefix(p.File().Filename(), sites.WorkingDir+string(os.PathSeparator)))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
},
|
||||
},
|
||||
&cobra.Command{
|
||||
Use: "future",
|
||||
Short: "List all posts dated in the future",
|
||||
Long: `List all of the posts in your content directory which will be
|
||||
posted in the future.`,
|
||||
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 {
|
||||
cfgInit := func(c *commandeer) error {
|
||||
c.Set("buildFuture", true)
|
||||
return nil
|
||||
}
|
||||
c, err := initializeConfig(true, false, &cc.hugoBuilderCommon, cc, cfgInit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sites, err := hugolib.NewHugoSites(*c.DepsCfg)
|
||||
sites, err := cc.buildSites(map[string]interface{}{"buildFuture": true})
|
||||
|
||||
if err != nil {
|
||||
return newSystemError("Error creating sites", err)
|
||||
return newSystemError("Error building sites", err)
|
||||
}
|
||||
|
||||
if err := sites.Build(hugolib.BuildCfg{SkipRender: true}); err != nil {
|
||||
return newSystemError("Error Processing Source Content", err)
|
||||
}
|
||||
writer := csv.NewWriter(os.Stdout)
|
||||
defer writer.Flush()
|
||||
|
||||
for _, p := range sites.Pages() {
|
||||
if p.IsFuture() {
|
||||
jww.FEEDBACK.Println(filepath.Join(p.File.Dir(), p.File.LogicalName()))
|
||||
if resource.IsFuture(p) {
|
||||
err := writer.Write([]string{
|
||||
strings.TrimPrefix(p.File().Filename(), sites.WorkingDir+string(os.PathSeparator)),
|
||||
p.PublishDate().Format(time.RFC3339),
|
||||
})
|
||||
if err != nil {
|
||||
return newSystemError("Error writing future posts to stdout", err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
},
|
||||
},
|
||||
&cobra.Command{
|
||||
Use: "expired",
|
||||
Short: "List all posts already expired",
|
||||
Long: `List all of the posts in your content directory which has already
|
||||
expired.`,
|
||||
Long: `List all of the posts in your content directory which has already expired.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfgInit := func(c *commandeer) error {
|
||||
c.Set("buildExpired", true)
|
||||
return nil
|
||||
}
|
||||
c, err := initializeConfig(true, false, &cc.hugoBuilderCommon, cc, cfgInit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sites, err := hugolib.NewHugoSites(*c.DepsCfg)
|
||||
sites, err := cc.buildSites(map[string]interface{}{"buildExpired": true})
|
||||
|
||||
if err != nil {
|
||||
return newSystemError("Error creating sites", err)
|
||||
return newSystemError("Error building sites", err)
|
||||
}
|
||||
|
||||
if err := sites.Build(hugolib.BuildCfg{SkipRender: true}); err != nil {
|
||||
return newSystemError("Error Processing Source Content", err)
|
||||
}
|
||||
writer := csv.NewWriter(os.Stdout)
|
||||
defer writer.Flush()
|
||||
|
||||
for _, p := range sites.Pages() {
|
||||
if p.IsExpired() {
|
||||
jww.FEEDBACK.Println(filepath.Join(p.File.Dir(), p.File.LogicalName()))
|
||||
if resource.IsExpired(p) {
|
||||
err := writer.Write([]string{
|
||||
strings.TrimPrefix(p.File().Filename(), sites.WorkingDir+string(os.PathSeparator)),
|
||||
p.ExpiryDate().Format(time.RFC3339),
|
||||
})
|
||||
if err != nil {
|
||||
return newSystemError("Error writing expired posts to stdout", err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
&cobra.Command{
|
||||
Use: "all",
|
||||
Short: "List all posts",
|
||||
Long: `List all of the posts in your content directory, include drafts, future and expired pages.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
sites, err := cc.buildSites(map[string]interface{}{
|
||||
"buildExpired": true,
|
||||
"buildDrafts": true,
|
||||
"buildFuture": true,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return newSystemError("Error building sites", err)
|
||||
}
|
||||
|
||||
writer := csv.NewWriter(os.Stdout)
|
||||
defer writer.Flush()
|
||||
|
||||
writer.Write([]string{
|
||||
"path",
|
||||
"slug",
|
||||
"title",
|
||||
"date",
|
||||
"expiryDate",
|
||||
"publishDate",
|
||||
"draft",
|
||||
"permalink",
|
||||
})
|
||||
for _, p := range sites.Pages() {
|
||||
if !p.IsPage() {
|
||||
continue
|
||||
}
|
||||
err := writer.Write([]string{
|
||||
strings.TrimPrefix(p.File().Filename(), sites.WorkingDir+string(os.PathSeparator)),
|
||||
p.Slug(),
|
||||
p.Title(),
|
||||
p.Date().Format(time.RFC3339),
|
||||
p.ExpiryDate().Format(time.RFC3339),
|
||||
p.PublishDate().Format(time.RFC3339),
|
||||
strconv.FormatBool(p.Draft()),
|
||||
p.Permalink(),
|
||||
})
|
||||
if err != nil {
|
||||
return newSystemError("Error writing posts to stdout", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/csv"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func captureStdout(f func() (*cobra.Command, error)) (string, error) {
|
||||
old := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
_, err := f()
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
w.Close()
|
||||
os.Stdout = old
|
||||
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, r)
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func TestListAll(t *testing.T) {
|
||||
assert := require.New(t)
|
||||
dir, err := createSimpleTestSite(t, testSiteConfig{})
|
||||
|
||||
assert.NoError(err)
|
||||
|
||||
hugoCmd := newCommandsBuilder().addAll().build()
|
||||
cmd := hugoCmd.getCommand()
|
||||
|
||||
defer func() {
|
||||
os.RemoveAll(dir)
|
||||
}()
|
||||
|
||||
cmd.SetArgs([]string{"-s=" + dir, "list", "all"})
|
||||
|
||||
out, err := captureStdout(cmd.ExecuteC)
|
||||
assert.NoError(err)
|
||||
|
||||
r := csv.NewReader(strings.NewReader(out))
|
||||
|
||||
header, err := r.Read()
|
||||
assert.NoError(err)
|
||||
|
||||
assert.Equal([]string{
|
||||
"path", "slug", "title",
|
||||
"date", "expiryDate", "publishDate",
|
||||
"draft", "permalink",
|
||||
}, header)
|
||||
|
||||
record, err := r.Read()
|
||||
assert.NoError(err)
|
||||
assert.Equal([]string{
|
||||
filepath.Join("content", "p1.md"), "", "P1",
|
||||
"0001-01-01T00:00:00Z", "0001-01-01T00:00:00Z", "0001-01-01T00:00:00Z",
|
||||
"false", "https://example.org/p1/",
|
||||
}, record)
|
||||
}
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
// 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.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/modules"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var _ cmder = (*modCmd)(nil)
|
||||
|
||||
type modCmd struct {
|
||||
*baseBuilderCmd
|
||||
}
|
||||
|
||||
func (b *commandsBuilder) newModCmd() *modCmd {
|
||||
c := &modCmd{}
|
||||
|
||||
const commonUsage = `
|
||||
Note that Hugo will always start out by resolving the components defined in the site
|
||||
configuration, provided by a _vendor directory (if no --ignoreVendor flag provided),
|
||||
Go Modules, or a folder inside the themes directory, in that order.
|
||||
|
||||
See https://gohugo.io/hugo-modules/ for more information.
|
||||
|
||||
`
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "mod",
|
||||
Short: "Various Hugo Modules helpers.",
|
||||
Long: `Various helpers to help manage the modules in your project's dependency graph.
|
||||
|
||||
Most operations here requires a Go version installed on your system (>= Go 1.12) and the relevant VCS client (typically Git).
|
||||
This is not needed if you only operate on modules inside /themes or if you have vendored them via "hugo mod vendor".
|
||||
|
||||
` + commonUsage,
|
||||
|
||||
RunE: nil,
|
||||
}
|
||||
|
||||
cmd.AddCommand(
|
||||
&cobra.Command{
|
||||
Use: "get",
|
||||
DisableFlagParsing: true,
|
||||
Short: "Resolves dependencies in your current Hugo Project.",
|
||||
Long: `
|
||||
Resolves dependencies in your current Hugo Project.
|
||||
|
||||
Some examples:
|
||||
|
||||
Install the latest version possible for a given module:
|
||||
|
||||
hugo mod get github.com/gohugoio/testshortcodes
|
||||
|
||||
Install a specific version:
|
||||
|
||||
hugo mod get github.com/gohugoio/testshortcodes@v0.3.0
|
||||
|
||||
Install the latest versions of all module dependencies:
|
||||
|
||||
hugo mod get -u
|
||||
|
||||
Run "go help get" for more information. All flags available for "go get" is also relevant here.
|
||||
` + commonUsage,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return c.withModsClient(false, func(c *modules.Client) error {
|
||||
// We currently just pass on the flags we get to Go and
|
||||
// need to do the flag handling manually.
|
||||
if len(args) == 1 && strings.Contains(args[0], "-h") {
|
||||
return cmd.Help()
|
||||
}
|
||||
return c.Get(args...)
|
||||
})
|
||||
},
|
||||
},
|
||||
&cobra.Command{
|
||||
Use: "graph",
|
||||
Short: "Print a module dependency graph.",
|
||||
Long: `Print a module dependency graph with information about module status (disabled, vendored).
|
||||
Note that for vendored modules, that is the version listed and not the one from go.mod.
|
||||
`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return c.withModsClient(true, func(c *modules.Client) error {
|
||||
return c.Graph(os.Stdout)
|
||||
})
|
||||
},
|
||||
},
|
||||
&cobra.Command{
|
||||
Use: "init",
|
||||
Short: "Initialize this project as a Hugo Module.",
|
||||
Long: `Initialize this project as a Hugo Module.
|
||||
It will try to guess the module path, but you may help by passing it as an argument, e.g:
|
||||
|
||||
hugo mod init github.com/gohugoio/testshortcodes
|
||||
|
||||
Note that Hugo Modules supports multi-module projects, so you can initialize a Hugo Module
|
||||
inside a subfolder on GitHub, as one example.
|
||||
`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
var path string
|
||||
if len(args) >= 1 {
|
||||
path = args[0]
|
||||
}
|
||||
return c.withModsClient(false, func(c *modules.Client) error {
|
||||
return c.Init(path)
|
||||
})
|
||||
},
|
||||
},
|
||||
&cobra.Command{
|
||||
Use: "vendor",
|
||||
Short: "Vendor all module dependencies into the _vendor directory.",
|
||||
Long: `Vendor all module dependencies into the _vendor directory.
|
||||
|
||||
If a module is vendored, that is where Hugo will look for it's dependencies.
|
||||
`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return c.withModsClient(true, func(c *modules.Client) error {
|
||||
return c.Vendor()
|
||||
})
|
||||
},
|
||||
},
|
||||
&cobra.Command{
|
||||
Use: "tidy",
|
||||
Short: "Remove unused entries in go.mod and go.sum.",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return c.withModsClient(true, func(c *modules.Client) error {
|
||||
return c.Tidy()
|
||||
})
|
||||
},
|
||||
},
|
||||
&cobra.Command{
|
||||
Use: "clean",
|
||||
Short: "Delete the entire Hugo Module cache.",
|
||||
Long: `Delete the entire Hugo Module cache.
|
||||
|
||||
Note that after you run this command, all of your dependencies will be re-downloaded next time you run "hugo".
|
||||
|
||||
Also note that if you configure a positive maxAge for the "modules" file cache, it will also be cleaned as part of "hugo --gc".
|
||||
|
||||
`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
com, err := c.initConfig(true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = com.hugo.FileCaches.ModulesCache().Prune(true)
|
||||
return err
|
||||
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
c.baseBuilderCmd = b.newBuilderCmd(cmd)
|
||||
|
||||
return c
|
||||
|
||||
}
|
||||
|
||||
func (c *modCmd) withModsClient(failOnMissingConfig bool, f func(*modules.Client) error) error {
|
||||
com, err := c.initConfig(failOnMissingConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return f(com.hugo.ModulesClient)
|
||||
}
|
||||
|
||||
func (c *modCmd) initConfig(failOnNoConfig bool) (*commandeer, error) {
|
||||
com, err := initializeConfig(failOnNoConfig, false, &c.hugoBuilderCommon, c, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return com, nil
|
||||
}
|
||||
+24
-49
@@ -30,16 +30,14 @@ import (
|
||||
var _ cmder = (*newCmd)(nil)
|
||||
|
||||
type newCmd struct {
|
||||
hugoBuilderCommon
|
||||
contentEditor string
|
||||
contentType string
|
||||
|
||||
*baseCmd
|
||||
*baseBuilderCmd
|
||||
}
|
||||
|
||||
func newNewCmd() *newCmd {
|
||||
cc := &newCmd{}
|
||||
cc.baseCmd = newBaseCmd(&cobra.Command{
|
||||
func (b *commandsBuilder) newNewCmd() *newCmd {
|
||||
cmd := &cobra.Command{
|
||||
Use: "new [path]",
|
||||
Short: "Create new content for your site",
|
||||
Long: `Create a new content file and automatically set the date and title.
|
||||
@@ -47,18 +45,20 @@ It will guess which kind of file to create based on the path provided.
|
||||
|
||||
You can also specify the kind with ` + "`-k KIND`" + `.
|
||||
|
||||
If archetypes are provided in your theme or site, they will be used.`,
|
||||
If archetypes are provided in your theme or site, they will be used.
|
||||
|
||||
RunE: cc.newContent,
|
||||
})
|
||||
Ensure you run this within the root directory of your site.`,
|
||||
}
|
||||
|
||||
cc.cmd.Flags().StringVarP(&cc.contentType, "kind", "k", "", "content type to create")
|
||||
cc.cmd.PersistentFlags().StringVarP(&cc.source, "source", "s", "", "filesystem path to read files relative from")
|
||||
cc.cmd.PersistentFlags().SetAnnotation("source", cobra.BashCompSubdirsInDir, []string{})
|
||||
cc.cmd.Flags().StringVar(&cc.contentEditor, "editor", "", "edit new content with this editor, if provided")
|
||||
cc := &newCmd{baseBuilderCmd: b.newBuilderCmd(cmd)}
|
||||
|
||||
cc.cmd.AddCommand(newNewSiteCmd().getCommand())
|
||||
cc.cmd.AddCommand(newNewThemeCmd().getCommand())
|
||||
cmd.Flags().StringVarP(&cc.contentType, "kind", "k", "", "content type to create")
|
||||
cmd.Flags().StringVar(&cc.contentEditor, "editor", "", "edit new content with this editor, if provided")
|
||||
|
||||
cmd.AddCommand(newNewSiteCmd().getCommand())
|
||||
cmd.AddCommand(newNewThemeCmd().getCommand())
|
||||
|
||||
cmd.RunE = cc.newContent
|
||||
|
||||
return cc
|
||||
}
|
||||
@@ -85,45 +85,13 @@ func (n *newCmd) newContent(cmd *cobra.Command, args []string) error {
|
||||
|
||||
var kind string
|
||||
|
||||
createPath, kind = newContentPathSection(createPath)
|
||||
createPath, kind = newContentPathSection(c.hugo, createPath)
|
||||
|
||||
if n.contentType != "" {
|
||||
kind = n.contentType
|
||||
}
|
||||
|
||||
cfg := c.DepsCfg
|
||||
|
||||
ps, err := helpers.NewPathSpec(cfg.Fs, cfg.Cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If a site isn't in use in the archetype template, we can skip the build.
|
||||
siteFactory := func(filename string, siteUsed bool) (*hugolib.Site, error) {
|
||||
if !siteUsed {
|
||||
return hugolib.NewSite(*cfg)
|
||||
}
|
||||
var s *hugolib.Site
|
||||
|
||||
if err := c.hugo.Build(hugolib.BuildCfg{SkipRender: true}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s = c.hugo.Sites[0]
|
||||
|
||||
if len(c.hugo.Sites) > 1 {
|
||||
// Find the best match.
|
||||
for _, ss := range c.hugo.Sites {
|
||||
if strings.Contains(createPath, "."+ss.Language.Lang) {
|
||||
s = ss
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
return create.NewContent(ps, siteFactory, kind, createPath)
|
||||
return create.NewContent(c.hugo, kind, createPath)
|
||||
}
|
||||
|
||||
func mkdir(x ...string) {
|
||||
@@ -144,10 +112,17 @@ func touchFile(fs afero.Fs, x ...string) {
|
||||
}
|
||||
}
|
||||
|
||||
func newContentPathSection(path string) (string, string) {
|
||||
func newContentPathSection(h *hugolib.HugoSites, path string) (string, string) {
|
||||
// Forward slashes is used in all examples. Convert if needed.
|
||||
// Issue #1133
|
||||
createpath := filepath.FromSlash(path)
|
||||
|
||||
if h != nil {
|
||||
for _, dir := range h.BaseFs.Content.Dirs {
|
||||
createpath = strings.TrimPrefix(createpath, dir.Meta().Filename())
|
||||
}
|
||||
}
|
||||
|
||||
var section string
|
||||
// assume the first directory is the section (kind)
|
||||
if strings.Contains(createpath[1:], helpers.FilePathSeparator) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
// 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.
|
||||
@@ -25,7 +25,7 @@ import (
|
||||
|
||||
// Issue #1133
|
||||
func TestNewContentPathSectionWithForwardSlashes(t *testing.T) {
|
||||
p, s := newContentPathSection("/post/new.md")
|
||||
p, s := newContentPathSection(nil, "/post/new.md")
|
||||
assert.Equal(t, filepath.FromSlash("/post/new.md"), p)
|
||||
assert.Equal(t, "post", s)
|
||||
}
|
||||
@@ -62,7 +62,7 @@ func TestDoNewSite_noerror_base_exists_but_empty(t *testing.T) {
|
||||
_, fs := newTestCfg()
|
||||
n := newNewSiteCmd()
|
||||
|
||||
require.NoError(t, fs.Source.MkdirAll(basepath, 777))
|
||||
require.NoError(t, fs.Source.MkdirAll(basepath, 0777))
|
||||
|
||||
require.NoError(t, n.doNewSite(fs, basepath, false))
|
||||
}
|
||||
@@ -72,7 +72,7 @@ func TestDoNewSite_error_base_exists(t *testing.T) {
|
||||
_, fs := newTestCfg()
|
||||
n := newNewSiteCmd()
|
||||
|
||||
require.NoError(t, fs.Source.MkdirAll(basepath, 777))
|
||||
require.NoError(t, fs.Source.MkdirAll(basepath, 0777))
|
||||
_, err := fs.Source.Create(filepath.Join(basepath, "foo"))
|
||||
require.NoError(t, err)
|
||||
// Since the directory already exists and isn't empty, expect an error
|
||||
@@ -85,7 +85,7 @@ func TestDoNewSite_force_empty_dir(t *testing.T) {
|
||||
_, fs := newTestCfg()
|
||||
n := newNewSiteCmd()
|
||||
|
||||
require.NoError(t, fs.Source.MkdirAll(basepath, 777))
|
||||
require.NoError(t, fs.Source.MkdirAll(basepath, 0777))
|
||||
|
||||
require.NoError(t, n.doNewSite(fs, basepath, true))
|
||||
|
||||
@@ -99,7 +99,7 @@ func TestDoNewSite_error_force_dir_inside_exists(t *testing.T) {
|
||||
|
||||
contentPath := filepath.Join(basepath, "content")
|
||||
|
||||
require.NoError(t, fs.Source.MkdirAll(contentPath, 777))
|
||||
require.NoError(t, fs.Source.MkdirAll(contentPath, 0777))
|
||||
require.Error(t, n.doNewSite(fs, basepath, true))
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ func TestDoNewSite_error_force_config_inside_exists(t *testing.T) {
|
||||
n := newNewSiteCmd()
|
||||
|
||||
configPath := filepath.Join(basepath, "config.toml")
|
||||
require.NoError(t, fs.Source.MkdirAll(basepath, 777))
|
||||
require.NoError(t, fs.Source.MkdirAll(basepath, 0777))
|
||||
_, err := fs.Source.Create(configPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
||||
@@ -16,10 +16,13 @@ package commands
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/parser/metadecoders"
|
||||
|
||||
_errors "github.com/pkg/errors"
|
||||
|
||||
"github.com/gohugoio/hugo/create"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
@@ -92,7 +95,7 @@ func (n *newSiteCmd) doNewSite(fs *hugofs.Fs, basepath string, force bool) error
|
||||
|
||||
for _, dir := range dirs {
|
||||
if err := fs.Source.MkdirAll(dir, 0777); err != nil {
|
||||
return fmt.Errorf("Failed to create dir: %s", err)
|
||||
return _errors.Wrap(err, "Failed to create dir")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,10 +133,9 @@ func createConfig(fs *hugofs.Fs, inpath string, kind string) (err error) {
|
||||
"title": "My New Hugo Site",
|
||||
"languageCode": "en-us",
|
||||
}
|
||||
kind = parser.FormatSanitize(kind)
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = parser.InterfaceToConfig(in, parser.FormatToLeadRune(kind), &buf)
|
||||
err = parser.InterfaceToConfig(in, metadecoders.FormatFromString(kind), &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -147,7 +149,7 @@ func nextStepsText() string {
|
||||
nextStepsText.WriteString(`Just a few more steps and you're ready to go:
|
||||
|
||||
1. Download a theme into the same-named folder.
|
||||
Choose a theme from https://themes.gohugo.io/, or
|
||||
Choose a theme from https://themes.gohugo.io/ or
|
||||
create your own with the "hugo new theme <THEMENAME>" command.
|
||||
2. Perhaps you want to add some content. You can add single files
|
||||
with "hugo new `)
|
||||
|
||||
+106
-49
@@ -1,4 +1,4 @@
|
||||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
// 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.
|
||||
@@ -14,6 +14,7 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -21,6 +22,7 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -28,10 +30,12 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/gohugoio/hugo/livereload"
|
||||
"github.com/gohugoio/hugo/tpl"
|
||||
|
||||
"github.com/gohugoio/hugo/config"
|
||||
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -52,7 +56,8 @@ type serverCmd struct {
|
||||
serverWatch bool
|
||||
noHTTPCache bool
|
||||
|
||||
disableFastRender bool
|
||||
disableFastRender bool
|
||||
disableBrowserError bool
|
||||
|
||||
*baseBuilderCmd
|
||||
}
|
||||
@@ -93,6 +98,7 @@ of a second, you will be able to save and see your changes nearly instantly.`,
|
||||
cc.cmd.Flags().BoolVar(&cc.navigateToChanged, "navigateToChanged", false, "navigate to changed content file on live browser reload")
|
||||
cc.cmd.Flags().BoolVar(&cc.renderToDisk, "renderToDisk", false, "render to Destination path (default is render to memory & serve from there)")
|
||||
cc.cmd.Flags().BoolVar(&cc.disableFastRender, "disableFastRender", false, "enables full re-renders on changes")
|
||||
cc.cmd.Flags().BoolVar(&cc.disableBrowserError, "disableBrowserError", false, "do not show build errors in the browser")
|
||||
|
||||
cc.cmd.Flags().String("memstats", "", "log memory usage to this file")
|
||||
cc.cmd.Flags().String("meminterval", "100ms", "interval to poll memory usage (requires --memstats), valid time units are \"ns\", \"us\" (or \"µs\"), \"ms\", \"s\", \"m\", \"h\".")
|
||||
@@ -122,27 +128,30 @@ func (f noDirFile) Readdir(count int) ([]os.FileInfo, error) {
|
||||
|
||||
var serverPorts []int
|
||||
|
||||
func (s *serverCmd) server(cmd *cobra.Command, args []string) error {
|
||||
func (sc *serverCmd) server(cmd *cobra.Command, args []string) error {
|
||||
// If a Destination is provided via flag write to disk
|
||||
destination, _ := cmd.Flags().GetString("destination")
|
||||
if destination != "" {
|
||||
s.renderToDisk = true
|
||||
sc.renderToDisk = true
|
||||
}
|
||||
|
||||
var serverCfgInit sync.Once
|
||||
|
||||
cfgInit := func(c *commandeer) error {
|
||||
c.Set("renderToMemory", !s.renderToDisk)
|
||||
c.Set("renderToMemory", !sc.renderToDisk)
|
||||
if cmd.Flags().Changed("navigateToChanged") {
|
||||
c.Set("navigateToChanged", s.navigateToChanged)
|
||||
c.Set("navigateToChanged", sc.navigateToChanged)
|
||||
}
|
||||
if cmd.Flags().Changed("disableLiveReload") {
|
||||
c.Set("disableLiveReload", s.disableLiveReload)
|
||||
c.Set("disableLiveReload", sc.disableLiveReload)
|
||||
}
|
||||
if cmd.Flags().Changed("disableFastRender") {
|
||||
c.Set("disableFastRender", s.disableFastRender)
|
||||
c.Set("disableFastRender", sc.disableFastRender)
|
||||
}
|
||||
if s.serverWatch {
|
||||
if cmd.Flags().Changed("disableBrowserError") {
|
||||
c.Set("disableBrowserError", sc.disableBrowserError)
|
||||
}
|
||||
if sc.serverWatch {
|
||||
c.Set("watch", true)
|
||||
}
|
||||
|
||||
@@ -158,25 +167,25 @@ func (s *serverCmd) server(cmd *cobra.Command, args []string) error {
|
||||
serverPorts = make([]int, 1)
|
||||
|
||||
if c.languages.IsMultihost() {
|
||||
if !s.serverAppend {
|
||||
if !sc.serverAppend {
|
||||
err = newSystemError("--appendPort=false not supported when in multihost mode")
|
||||
}
|
||||
serverPorts = make([]int, len(c.languages))
|
||||
}
|
||||
|
||||
currentServerPort := s.serverPort
|
||||
currentServerPort := sc.serverPort
|
||||
|
||||
for i := 0; i < len(serverPorts); i++ {
|
||||
l, err := net.Listen("tcp", net.JoinHostPort(s.serverInterface, strconv.Itoa(currentServerPort)))
|
||||
l, err := net.Listen("tcp", net.JoinHostPort(sc.serverInterface, strconv.Itoa(currentServerPort)))
|
||||
if err == nil {
|
||||
l.Close()
|
||||
serverPorts[i] = currentServerPort
|
||||
} else {
|
||||
if i == 0 && s.cmd.Flags().Changed("port") {
|
||||
if i == 0 && sc.cmd.Flags().Changed("port") {
|
||||
// port set explicitly by user -- he/she probably meant it!
|
||||
err = newSystemErrorF("Server startup failed: %s", err)
|
||||
}
|
||||
jww.ERROR.Println("port", s.serverPort, "already in use, attempting to use an available port")
|
||||
c.logger.FEEDBACK.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)
|
||||
@@ -190,9 +199,9 @@ func (s *serverCmd) server(cmd *cobra.Command, args []string) error {
|
||||
|
||||
c.serverPorts = serverPorts
|
||||
|
||||
c.Set("port", s.serverPort)
|
||||
if s.liveReloadPort != -1 {
|
||||
c.Set("liveReloadPort", s.liveReloadPort)
|
||||
c.Set("port", sc.serverPort)
|
||||
if sc.liveReloadPort != -1 {
|
||||
c.Set("liveReloadPort", sc.liveReloadPort)
|
||||
} else {
|
||||
c.Set("liveReloadPort", serverPorts[0])
|
||||
}
|
||||
@@ -206,7 +215,7 @@ func (s *serverCmd) server(cmd *cobra.Command, args []string) error {
|
||||
serverPort = serverPorts[0]
|
||||
}
|
||||
|
||||
baseURL, err := s.fixURL(language, s.baseURL, serverPort)
|
||||
baseURL, err := sc.fixURL(language, sc.baseURL, serverPort)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -223,10 +232,10 @@ func (s *serverCmd) server(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
if err := memStats(); err != nil {
|
||||
jww.ERROR.Println("memstats error:", err)
|
||||
jww.WARN.Println("memstats error:", err)
|
||||
}
|
||||
|
||||
c, err := initializeConfig(true, true, &s.hugoBuilderCommon, s, cfgInit)
|
||||
c, err := initializeConfig(true, true, &sc.hugoBuilderCommon, sc, cfgInit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -240,22 +249,18 @@ func (s *serverCmd) server(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
// Watch runs its own server as part of the routine
|
||||
if s.serverWatch {
|
||||
if sc.serverWatch {
|
||||
|
||||
watchDirs, err := c.getDirList()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
baseWatchDir := c.Cfg.GetString("workingDir")
|
||||
relWatchDirs := make([]string, len(watchDirs))
|
||||
for i, dir := range watchDirs {
|
||||
relWatchDirs[i], _ = helpers.GetRelativePath(dir, baseWatchDir)
|
||||
watchGroups := helpers.ExtractAndGroupRootPaths(watchDirs)
|
||||
|
||||
for _, group := range watchGroups {
|
||||
jww.FEEDBACK.Printf("Watching for changes in %s\n", group)
|
||||
}
|
||||
|
||||
rootWatchDirs := strings.Join(helpers.UniqueStrings(helpers.ExtractRootPaths(relWatchDirs)), ",")
|
||||
|
||||
jww.FEEDBACK.Printf("Watching for changes in %s%s{%s}\n", baseWatchDir, helpers.FilePathSeparator, rootWatchDirs)
|
||||
watcher, err := c.newWatcher(watchDirs...)
|
||||
|
||||
if err != nil {
|
||||
@@ -266,15 +271,25 @@ func (s *serverCmd) server(cmd *cobra.Command, args []string) error {
|
||||
|
||||
}
|
||||
|
||||
return c.serve(s)
|
||||
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)
|
||||
}
|
||||
|
||||
return strings.Join(helpers.UniqueStringsSorted(helpers.ExtractRootPaths(relWatchDirs)), ",")
|
||||
}
|
||||
|
||||
type fileServer struct {
|
||||
baseURLs []string
|
||||
roots []string
|
||||
c *commandeer
|
||||
s *serverCmd
|
||||
baseURLs []string
|
||||
roots []string
|
||||
errorTemplate tpl.Template
|
||||
c *commandeer
|
||||
s *serverCmd
|
||||
}
|
||||
|
||||
func (f *fileServer) createEndpoint(i int) (*http.ServeMux, string, string, error) {
|
||||
@@ -290,6 +305,8 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, string, string, erro
|
||||
|
||||
absPublishDir := f.c.hugo.PathSpec.AbsPathify(publishDir)
|
||||
|
||||
jww.FEEDBACK.Printf("Environment: %q", f.c.hugo.Deps.Site.Hugo().Environment)
|
||||
|
||||
if i == 0 {
|
||||
if f.s.renderToDisk {
|
||||
jww.FEEDBACK.Println("Serving pages from " + absPublishDir)
|
||||
@@ -301,30 +318,59 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, string, string, erro
|
||||
httpFs := afero.NewHttpFs(f.c.destinationFs)
|
||||
fs := filesOnlyFs{httpFs.Dir(absPublishDir)}
|
||||
|
||||
doLiveReload := !f.s.buildWatch && !f.c.Cfg.GetBool("disableLiveReload")
|
||||
fastRenderMode := doLiveReload && !f.c.Cfg.GetBool("disableFastRender")
|
||||
|
||||
if i == 0 && fastRenderMode {
|
||||
if i == 0 && f.c.fastRenderMode {
|
||||
jww.FEEDBACK.Println("Running in Fast Render Mode. For full rebuilds on change: hugo server --disableFastRender")
|
||||
}
|
||||
|
||||
// We're only interested in the path
|
||||
u, err := url.Parse(baseURL)
|
||||
if err != nil {
|
||||
return nil, "", "", fmt.Errorf("Invalid baseURL: %s", err)
|
||||
return nil, "", "", errors.Wrap(err, "Invalid baseURL")
|
||||
}
|
||||
|
||||
decorate := func(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if f.c.showErrorInBrowser {
|
||||
// First check the error state
|
||||
err := f.c.getErrorWithContext()
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
var b bytes.Buffer
|
||||
err := f.errorTemplate.Execute(&b, err)
|
||||
if err != nil {
|
||||
f.c.logger.ERROR.Println(err)
|
||||
}
|
||||
port = 1313
|
||||
if !f.c.paused {
|
||||
port = f.c.Cfg.GetInt("liveReloadPort")
|
||||
}
|
||||
fmt.Fprint(w, injectLiveReloadScript(&b, port))
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if f.s.noHTTPCache {
|
||||
w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0")
|
||||
w.Header().Set("Pragma", "no-cache")
|
||||
}
|
||||
|
||||
if fastRenderMode {
|
||||
if f.c.fastRenderMode && f.c.buildErr == nil {
|
||||
p := r.RequestURI
|
||||
if strings.HasSuffix(p, "/") || strings.HasSuffix(p, "html") || strings.HasSuffix(p, "htm") {
|
||||
if !f.c.visitedURLs.Contains(p) {
|
||||
// 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 f.c.showErrorInBrowser {
|
||||
http.Redirect(w, r, p, http.StatusMovedPermanently)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
f.c.visitedURLs.Add(p)
|
||||
|
||||
}
|
||||
}
|
||||
h.ServeHTTP(w, r)
|
||||
@@ -345,6 +391,11 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, string, string, erro
|
||||
return mu, u.String(), endpoint, nil
|
||||
}
|
||||
|
||||
var logErrorRe = regexp.MustCompile(`(?s)ERROR \d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} `)
|
||||
|
||||
func removeErrorPrefixFromLog(content string) string {
|
||||
return logErrorRe.ReplaceAllLiteralString(content, "")
|
||||
}
|
||||
func (c *commandeer) serve(s *serverCmd) error {
|
||||
|
||||
isMultiHost := c.hugo.IsMultihost()
|
||||
@@ -357,7 +408,7 @@ func (c *commandeer) serve(s *serverCmd) error {
|
||||
if isMultiHost {
|
||||
for _, s := range c.hugo.Sites {
|
||||
baseURLs = append(baseURLs, s.BaseURL.String())
|
||||
roots = append(roots, s.Language.Lang)
|
||||
roots = append(roots, s.Language().Lang)
|
||||
}
|
||||
} else {
|
||||
s := c.hugo.Sites[0]
|
||||
@@ -365,11 +416,17 @@ func (c *commandeer) serve(s *serverCmd) error {
|
||||
roots = []string{""}
|
||||
}
|
||||
|
||||
templ, err := c.hugo.TextTmpl.Parse("__default_server_error", buildErrorTemplate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
srv := &fileServer{
|
||||
baseURLs: baseURLs,
|
||||
roots: roots,
|
||||
c: c,
|
||||
s: s,
|
||||
baseURLs: baseURLs,
|
||||
roots: roots,
|
||||
c: c,
|
||||
s: s,
|
||||
errorTemplate: templ,
|
||||
}
|
||||
|
||||
doLiveReload := !c.Cfg.GetBool("disableLiveReload")
|
||||
@@ -378,7 +435,7 @@ func (c *commandeer) serve(s *serverCmd) error {
|
||||
livereload.Initialize()
|
||||
}
|
||||
|
||||
var sigs = make(chan os.Signal)
|
||||
var sigs = make(chan os.Signal, 1)
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
for i := range baseURLs {
|
||||
@@ -392,7 +449,7 @@ func (c *commandeer) serve(s *serverCmd) error {
|
||||
go func() {
|
||||
err = http.ListenAndServe(endpoint, mu)
|
||||
if err != nil {
|
||||
jww.ERROR.Printf("Error: %s\n", err.Error())
|
||||
c.logger.ERROR.Printf("Error: %s\n", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
@@ -453,7 +510,7 @@ func (sc *serverCmd) fixURL(cfg config.Provider, s string, port int) (string, er
|
||||
if strings.Contains(u.Host, ":") {
|
||||
u.Host, _, err = net.SplitHostPort(u.Host)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Failed to split baseURL hostpost: %s", err)
|
||||
return "", errors.Wrap(err, "Failed to split baseURL hostpost")
|
||||
}
|
||||
}
|
||||
u.Host += fmt.Sprintf(":%d", port)
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
// Copyright 2018 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 (
|
||||
"bytes"
|
||||
"io"
|
||||
|
||||
"github.com/gohugoio/hugo/transform"
|
||||
"github.com/gohugoio/hugo/transform/livereloadinject"
|
||||
)
|
||||
|
||||
var buildErrorTemplate = `<!doctype html>
|
||||
<html class="no-js" lang="">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Hugo Server: Error</title>
|
||||
<style type="text/css">
|
||||
body {
|
||||
font-family: "Muli",avenir, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
font-size: 16px;
|
||||
background-color: black;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
main {
|
||||
margin: auto;
|
||||
width: 95%;
|
||||
padding: 1rem;
|
||||
}
|
||||
.version {
|
||||
color: #ccc;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
.stack {
|
||||
margin-top: 6rem;
|
||||
}
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
white-space: -moz-pre-wrap;
|
||||
white-space: -pre-wrap;
|
||||
white-space: -o-pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.highlight {
|
||||
overflow-x: auto;
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
background-color: #272822;
|
||||
border: 1px solid black;
|
||||
}
|
||||
a {
|
||||
color: #0594cb;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
color: #ccc;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
{{ highlight .Error "apl" "noclasses=true,style=monokai" }}
|
||||
{{ with .File }}
|
||||
{{ $params := printf "noclasses=true,style=monokai,linenos=table,hl_lines=%d,linenostart=%d" (add .LinesPos 1) (sub .Position.LineNumber .LinesPos) }}
|
||||
{{ $lexer := .ChromaLexer | default "go-html-template" }}
|
||||
{{ highlight (delimit .Lines "\n") $lexer $params }}
|
||||
{{ end }}
|
||||
{{ with .StackTrace }}
|
||||
{{ highlight . "apl" "noclasses=true,style=monokai" }}
|
||||
{{ end }}
|
||||
<p class="version">{{ .Version }}</p>
|
||||
<a href="">Reload Page</a>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
func injectLiveReloadScript(src io.Reader, port int) string {
|
||||
var b bytes.Buffer
|
||||
chain := transform.Chain{livereloadinject.New(port)}
|
||||
chain.Apply(&b, src)
|
||||
|
||||
return b.String()
|
||||
}
|
||||
+32
-16
@@ -18,6 +18,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -33,7 +34,7 @@ func TestServer(t *testing.T) {
|
||||
t.Skip("Skip server test on appveyor")
|
||||
}
|
||||
assert := require.New(t)
|
||||
dir, err := createSimpleTestSite(t)
|
||||
dir, err := createSimpleTestSite(t, testSiteConfig{})
|
||||
assert.NoError(err)
|
||||
|
||||
// Let us hope that this port is available on all systems ...
|
||||
@@ -67,6 +68,7 @@ func TestServer(t *testing.T) {
|
||||
homeContent := helpers.ReaderToString(resp.Body)
|
||||
|
||||
assert.Contains(homeContent, "List: Hugo Commands")
|
||||
assert.Contains(homeContent, "Environment: development")
|
||||
|
||||
// Stop the server.
|
||||
stop <- true
|
||||
@@ -95,24 +97,38 @@ func TestFixURL(t *testing.T) {
|
||||
{"No config", "", "", true, 1313, "//localhost:1313/"},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
b := newCommandsBuilder()
|
||||
s := b.newServerCmd()
|
||||
v := viper.New()
|
||||
baseURL := test.CLIBaseURL
|
||||
v.Set("baseURL", test.CfgBaseURL)
|
||||
s.serverAppend = test.AppendPort
|
||||
s.serverPort = test.Port
|
||||
result, err := s.fixURL(v, baseURL, s.serverPort)
|
||||
if err != nil {
|
||||
t.Errorf("Test #%d %s: unexpected error %s", i, test.TestName, err)
|
||||
}
|
||||
if result != test.Result {
|
||||
t.Errorf("Test #%d %s: expected %q, got %q", i, test.TestName, test.Result, result)
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.TestName, func(t *testing.T) {
|
||||
b := newCommandsBuilder()
|
||||
s := b.newServerCmd()
|
||||
v := viper.New()
|
||||
baseURL := test.CLIBaseURL
|
||||
v.Set("baseURL", test.CfgBaseURL)
|
||||
s.serverAppend = test.AppendPort
|
||||
s.serverPort = test.Port
|
||||
result, err := s.fixURL(v, baseURL, s.serverPort)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error %s", err)
|
||||
}
|
||||
if result != test.Result {
|
||||
t.Errorf("Expected %q, got %q", test.Result, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveErrorPrefixFromLog(t *testing.T) {
|
||||
assert := require.New(t)
|
||||
content := `ERROR 2018/10/07 13:11:12 Error while rendering "home": template: _default/baseof.html:4:3: executing "main" at <partial "logo" .>: error calling partial: template: partials/logo.html:5:84: executing "partials/logo.html" at <$resized.AHeight>: can't evaluate field AHeight in type *resource.Image
|
||||
ERROR 2018/10/07 13:11:12 Rebuild failed: logged 1 error(s)
|
||||
`
|
||||
|
||||
withoutError := removeErrorPrefixFromLog(content)
|
||||
|
||||
assert.False(strings.Contains(withoutError, "ERROR"), withoutError)
|
||||
|
||||
}
|
||||
|
||||
func isWindowsCI() bool {
|
||||
return runtime.GOOS == "windows" && os.Getenv("CI") != ""
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ func (s *staticSyncer) syncsStaticEvents(staticEvents []fsnotify.Event) error {
|
||||
syncer := fsync.NewSyncer()
|
||||
syncer.NoTimes = c.Cfg.GetBool("noTimes")
|
||||
syncer.NoChmod = c.Cfg.GetBool("noChmod")
|
||||
syncer.ChmodFilter = chmodFilter
|
||||
syncer.SrcFs = sourceFs.Fs
|
||||
syncer.DestFs = c.Fs.Destination
|
||||
|
||||
@@ -105,10 +106,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.ERROR.Println(err)
|
||||
}
|
||||
} else {
|
||||
c.Logger.ERROR.Println(err)
|
||||
c.logger.ERROR.Println(err)
|
||||
}
|
||||
|
||||
continue
|
||||
@@ -117,7 +118,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.ERROR.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-26
@@ -14,12 +14,7 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
"github.com/gohugoio/hugo/resource/tocss/scss"
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
"github.com/spf13/cobra"
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
)
|
||||
@@ -45,24 +40,5 @@ func newVersionCmd() *versionCmd {
|
||||
}
|
||||
|
||||
func printHugoVersion() {
|
||||
program := "Hugo Static Site Generator"
|
||||
|
||||
version := "v" + helpers.CurrentHugoVersion.String()
|
||||
if hugolib.CommitHash != "" {
|
||||
version += "-" + strings.ToUpper(hugolib.CommitHash)
|
||||
}
|
||||
if scss.Supports() {
|
||||
version += "/extended"
|
||||
}
|
||||
|
||||
osArch := runtime.GOOS + "/" + runtime.GOARCH
|
||||
|
||||
var buildDate string
|
||||
if hugolib.BuildDate != "" {
|
||||
buildDate = hugolib.BuildDate
|
||||
} else {
|
||||
buildDate = "unknown"
|
||||
}
|
||||
|
||||
jww.FEEDBACK.Println(program, version, osArch, "BuildDate:", buildDate)
|
||||
jww.FEEDBACK.Println(hugo.BuildVersionString())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
// 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.
|
||||
|
||||
package collections
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// Append appends from to a slice to and returns the resulting slice.
|
||||
// If length of from is one and the only element is a slice of same type as to,
|
||||
// it will be appended.
|
||||
func Append(to interface{}, from ...interface{}) (interface{}, error) {
|
||||
tov, toIsNil := indirect(reflect.ValueOf(to))
|
||||
|
||||
toIsNil = toIsNil || to == nil
|
||||
var tot reflect.Type
|
||||
|
||||
if !toIsNil {
|
||||
if tov.Kind() != reflect.Slice {
|
||||
return nil, fmt.Errorf("expected a slice, got %T", to)
|
||||
}
|
||||
|
||||
tot = tov.Type().Elem()
|
||||
toIsNil = tov.Len() == 0
|
||||
|
||||
if len(from) == 1 {
|
||||
fromv := reflect.ValueOf(from[0])
|
||||
if fromv.Kind() == reflect.Slice {
|
||||
if toIsNil {
|
||||
// If we get nil []string, we just return the []string
|
||||
return from[0], nil
|
||||
}
|
||||
|
||||
fromt := reflect.TypeOf(from[0]).Elem()
|
||||
|
||||
// If we get []string []string, we append the from slice to to
|
||||
if tot == fromt {
|
||||
return reflect.AppendSlice(tov, fromv).Interface(), nil
|
||||
} else if !fromt.AssignableTo(tot) {
|
||||
// Fall back to a []interface{} slice.
|
||||
return appendToInterfaceSliceFromValues(tov, fromv)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if toIsNil {
|
||||
return Slice(from...), nil
|
||||
}
|
||||
|
||||
for _, f := range from {
|
||||
fv := reflect.ValueOf(f)
|
||||
if !fv.Type().AssignableTo(tot) {
|
||||
// Fall back to a []interface{} slice.
|
||||
return appendToInterfaceSlice(tov, from...)
|
||||
}
|
||||
tov = reflect.Append(tov, fv)
|
||||
}
|
||||
|
||||
return tov.Interface(), nil
|
||||
}
|
||||
|
||||
func appendToInterfaceSliceFromValues(slice1, slice2 reflect.Value) ([]interface{}, error) {
|
||||
var tos []interface{}
|
||||
|
||||
for _, slice := range []reflect.Value{slice1, slice2} {
|
||||
for i := 0; i < slice.Len(); i++ {
|
||||
tos = append(tos, slice.Index(i).Interface())
|
||||
}
|
||||
}
|
||||
|
||||
return tos, nil
|
||||
}
|
||||
|
||||
func appendToInterfaceSlice(tov reflect.Value, from ...interface{}) ([]interface{}, error) {
|
||||
var tos []interface{}
|
||||
|
||||
for i := 0; i < tov.Len(); i++ {
|
||||
tos = append(tos, tov.Index(i).Interface())
|
||||
}
|
||||
|
||||
tos = append(tos, from...)
|
||||
|
||||
return tos, nil
|
||||
}
|
||||
|
||||
// indirect is borrowed from the Go stdlib: 'text/template/exec.go'
|
||||
// TODO(bep) consolidate
|
||||
func indirect(v reflect.Value) (rv reflect.Value, isNil bool) {
|
||||
for ; v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface; v = v.Elem() {
|
||||
if v.IsNil() {
|
||||
return v, true
|
||||
}
|
||||
if v.Kind() == reflect.Interface && v.NumMethod() > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return v, false
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// Copyright 2018 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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAppend(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for i, test := range []struct {
|
||||
start interface{}
|
||||
addend []interface{}
|
||||
expected interface{}
|
||||
}{
|
||||
{[]string{"a", "b"}, []interface{}{"c"}, []string{"a", "b", "c"}},
|
||||
{[]string{"a", "b"}, []interface{}{"c", "d", "e"}, []string{"a", "b", "c", "d", "e"}},
|
||||
{[]string{"a", "b"}, []interface{}{[]string{"c", "d", "e"}}, []string{"a", "b", "c", "d", "e"}},
|
||||
{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"}},
|
||||
[]interface{}{&tstSlicer{"c"}},
|
||||
tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}, &tstSlicer{"c"}}},
|
||||
{&tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}},
|
||||
[]interface{}{&tstSlicer{"c"}},
|
||||
tstSlicers{&tstSlicer{"a"},
|
||||
&tstSlicer{"b"},
|
||||
&tstSlicer{"c"}}},
|
||||
{testSlicerInterfaces{&tstSlicerIn1{"a"}, &tstSlicerIn1{"b"}},
|
||||
[]interface{}{&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"}}},
|
||||
// Errors
|
||||
{"", []interface{}{[]string{"a", "b"}}, false},
|
||||
// No string concatenation.
|
||||
{"ab",
|
||||
[]interface{}{"c"},
|
||||
false},
|
||||
} {
|
||||
|
||||
errMsg := fmt.Sprintf("[%d]", i)
|
||||
|
||||
result, err := Append(test.start, test.addend...)
|
||||
|
||||
if b, ok := test.expected.(bool); ok && !b {
|
||||
require.Error(t, err, errMsg)
|
||||
continue
|
||||
}
|
||||
|
||||
require.NoError(t, err, errMsg)
|
||||
|
||||
if !reflect.DeepEqual(test.expected, result) {
|
||||
t.Fatalf("%s got\n%T: %v\nexpected\n%T: %v", errMsg, result, result, test.expected, test.expected)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright 2018 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 contains common Hugo functionality related to collection
|
||||
// handling.
|
||||
package collections
|
||||
|
||||
// Grouper defines a very generic way to group items by a given key.
|
||||
type Grouper interface {
|
||||
Group(key interface{}, items interface{}) (interface{}, error)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// Copyright 2018 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
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// Slicer defines a very generic way to create a typed slice. This is used
|
||||
// in collections.Slice template func to get types such as Pages, PageGroups etc.
|
||||
// instead of the less useful []interface{}.
|
||||
type Slicer interface {
|
||||
Slice(items interface{}) (interface{}, error)
|
||||
}
|
||||
|
||||
// Slice returns a slice of all passed arguments.
|
||||
func Slice(args ...interface{}) interface{} {
|
||||
if len(args) == 0 {
|
||||
return args
|
||||
}
|
||||
|
||||
first := args[0]
|
||||
firstType := reflect.TypeOf(first)
|
||||
|
||||
if firstType == nil {
|
||||
return args
|
||||
}
|
||||
|
||||
if g, ok := first.(Slicer); ok {
|
||||
v, err := g.Slice(args)
|
||||
if err == nil {
|
||||
return v
|
||||
}
|
||||
|
||||
// If Slice fails, the items are not of the same type and
|
||||
// []interface{} is the best we can do.
|
||||
return args
|
||||
}
|
||||
|
||||
if len(args) > 1 {
|
||||
// This can be a mix of types.
|
||||
for i := 1; i < len(args); i++ {
|
||||
if firstType != reflect.TypeOf(args[i]) {
|
||||
// []interface{} is the best we can do
|
||||
return args
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
slice := reflect.MakeSlice(reflect.SliceOf(firstType), len(args), len(args))
|
||||
for i, arg := range args {
|
||||
slice.Index(i).Set(reflect.ValueOf(arg))
|
||||
}
|
||||
return slice.Interface()
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
// 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.
|
||||
|
||||
package collections
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/alecthomas/assert"
|
||||
)
|
||||
|
||||
var _ Slicer = (*tstSlicer)(nil)
|
||||
var _ Slicer = (*tstSlicerIn1)(nil)
|
||||
var _ Slicer = (*tstSlicerIn2)(nil)
|
||||
var _ testSlicerInterface = (*tstSlicerIn1)(nil)
|
||||
var _ testSlicerInterface = (*tstSlicerIn1)(nil)
|
||||
|
||||
type testSlicerInterface interface {
|
||||
Name() string
|
||||
}
|
||||
|
||||
type testSlicerInterfaces []testSlicerInterface
|
||||
|
||||
type tstSlicerIn1 struct {
|
||||
name string
|
||||
}
|
||||
|
||||
type tstSlicerIn2 struct {
|
||||
name string
|
||||
}
|
||||
|
||||
type tstSlicer struct {
|
||||
name string
|
||||
}
|
||||
|
||||
func (p *tstSlicerIn1) Slice(in interface{}) (interface{}, error) {
|
||||
items := in.([]interface{})
|
||||
result := make(testSlicerInterfaces, len(items))
|
||||
for i, v := range items {
|
||||
switch vv := v.(type) {
|
||||
case testSlicerInterface:
|
||||
result[i] = vv
|
||||
default:
|
||||
return nil, errors.New("invalid type")
|
||||
}
|
||||
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (p *tstSlicerIn2) Slice(in interface{}) (interface{}, error) {
|
||||
items := in.([]interface{})
|
||||
result := make(testSlicerInterfaces, len(items))
|
||||
for i, v := range items {
|
||||
switch vv := v.(type) {
|
||||
case testSlicerInterface:
|
||||
result[i] = vv
|
||||
default:
|
||||
return nil, errors.New("invalid type")
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (p *tstSlicerIn1) Name() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
func (p *tstSlicerIn2) Name() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
func (p *tstSlicer) Slice(in interface{}) (interface{}, error) {
|
||||
items := in.([]interface{})
|
||||
result := make(tstSlicers, len(items))
|
||||
for i, v := range items {
|
||||
switch vv := v.(type) {
|
||||
case *tstSlicer:
|
||||
result[i] = vv
|
||||
default:
|
||||
return nil, errors.New("invalid type")
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type tstSlicers []*tstSlicer
|
||||
|
||||
func TestSlice(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for i, test := range []struct {
|
||||
args []interface{}
|
||||
expected interface{}
|
||||
}{
|
||||
{[]interface{}{"a", "b"}, []string{"a", "b"}},
|
||||
{[]interface{}{&tstSlicer{"a"}, &tstSlicer{"b"}}, tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}}},
|
||||
{[]interface{}{&tstSlicer{"a"}, "b"}, []interface{}{&tstSlicer{"a"}, "b"}},
|
||||
{[]interface{}{}, []interface{}{}},
|
||||
{[]interface{}{nil}, []interface{}{nil}},
|
||||
{[]interface{}{5, "b"}, []interface{}{5, "b"}},
|
||||
{[]interface{}{&tstSlicerIn1{"a"}, &tstSlicerIn2{"b"}}, testSlicerInterfaces{&tstSlicerIn1{"a"}, &tstSlicerIn2{"b"}}},
|
||||
{[]interface{}{&tstSlicerIn1{"a"}, &tstSlicer{"b"}}, []interface{}{&tstSlicerIn1{"a"}, &tstSlicer{"b"}}},
|
||||
} {
|
||||
errMsg := fmt.Sprintf("[%d] %v", i, test.args)
|
||||
|
||||
result := Slice(test.args...)
|
||||
|
||||
assert.Equal(t, test.expected, result, errMsg)
|
||||
}
|
||||
|
||||
assert.Len(t, Slice(), 0)
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
// Copyright 2018 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 errors contains common Hugo errors and error related utilities.
|
||||
package herrors
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/common/text"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
// LineMatcher contains the elements used to match an error to a line
|
||||
type LineMatcher struct {
|
||||
Position text.Position
|
||||
Error error
|
||||
|
||||
LineNumber int
|
||||
Offset int
|
||||
Line string
|
||||
}
|
||||
|
||||
// LineMatcherFn is used to match a line with an error.
|
||||
type LineMatcherFn func(m LineMatcher) bool
|
||||
|
||||
// SimpleLineMatcher simply matches by line number.
|
||||
var SimpleLineMatcher = func(m LineMatcher) bool {
|
||||
return m.Position.LineNumber == m.LineNumber
|
||||
}
|
||||
|
||||
var _ text.Positioner = ErrorContext{}
|
||||
|
||||
// ErrorContext contains contextual information about an error. This will
|
||||
// typically be the lines surrounding some problem in a file.
|
||||
type ErrorContext struct {
|
||||
|
||||
// If a match will contain the matched line and up to 2 lines before and after.
|
||||
// Will be empty if no match.
|
||||
Lines []string
|
||||
|
||||
// The position of the error in the Lines above. 0 based.
|
||||
LinesPos int
|
||||
|
||||
position text.Position
|
||||
|
||||
// The lexer to use for syntax highlighting.
|
||||
// https://gohugo.io/content-management/syntax-highlighting/#list-of-chroma-highlighting-languages
|
||||
ChromaLexer string
|
||||
}
|
||||
|
||||
// Position returns the text position of this error.
|
||||
func (e ErrorContext) Position() text.Position {
|
||||
return e.position
|
||||
}
|
||||
|
||||
var _ causer = (*ErrorWithFileContext)(nil)
|
||||
|
||||
// ErrorWithFileContext is an error with some additional file context related
|
||||
// to that error.
|
||||
type ErrorWithFileContext struct {
|
||||
cause error
|
||||
ErrorContext
|
||||
}
|
||||
|
||||
func (e *ErrorWithFileContext) Error() string {
|
||||
pos := e.Position()
|
||||
if pos.IsValid() {
|
||||
return pos.String() + ": " + e.cause.Error()
|
||||
}
|
||||
return e.cause.Error()
|
||||
}
|
||||
|
||||
func (e *ErrorWithFileContext) Cause() error {
|
||||
return e.cause
|
||||
}
|
||||
|
||||
// WithFileContextForFile will try to add a file context with lines matching the given matcher.
|
||||
// If no match could be found, the original error is returned with false as the second return value.
|
||||
func WithFileContextForFile(e error, realFilename, filename string, fs afero.Fs, matcher LineMatcherFn) (error, bool) {
|
||||
f, err := fs.Open(filename)
|
||||
if err != nil {
|
||||
return e, false
|
||||
}
|
||||
defer f.Close()
|
||||
return WithFileContext(e, realFilename, f, matcher)
|
||||
}
|
||||
|
||||
// WithFileContextForFile will try to add a file context with lines matching the given matcher.
|
||||
// If no match could be found, the original error is returned with false as the second return value.
|
||||
func WithFileContext(e error, realFilename string, r io.Reader, matcher LineMatcherFn) (error, bool) {
|
||||
if e == nil {
|
||||
panic("error missing")
|
||||
}
|
||||
le := UnwrapFileError(e)
|
||||
|
||||
if le == nil {
|
||||
var ok bool
|
||||
if le, ok = ToFileError("", e).(FileError); !ok {
|
||||
return e, false
|
||||
}
|
||||
}
|
||||
|
||||
var errCtx ErrorContext
|
||||
|
||||
posle := le.Position()
|
||||
|
||||
if posle.Offset != -1 {
|
||||
errCtx = locateError(r, le, func(m LineMatcher) bool {
|
||||
if posle.Offset >= m.Offset && posle.Offset < m.Offset+len(m.Line) {
|
||||
lno := posle.LineNumber - m.Position.LineNumber + m.LineNumber
|
||||
m.Position = text.Position{LineNumber: lno}
|
||||
}
|
||||
return matcher(m)
|
||||
})
|
||||
} else {
|
||||
errCtx = locateError(r, le, matcher)
|
||||
}
|
||||
|
||||
pos := &errCtx.position
|
||||
|
||||
if pos.LineNumber == -1 {
|
||||
return e, false
|
||||
}
|
||||
|
||||
pos.Filename = realFilename
|
||||
|
||||
if le.Type() != "" {
|
||||
errCtx.ChromaLexer = chromaLexerFromType(le.Type())
|
||||
} else {
|
||||
errCtx.ChromaLexer = chromaLexerFromFilename(realFilename)
|
||||
}
|
||||
|
||||
return &ErrorWithFileContext{cause: e, ErrorContext: errCtx}, true
|
||||
}
|
||||
|
||||
// UnwrapErrorWithFileContext tries to unwrap an ErrorWithFileContext from err.
|
||||
// It returns nil if this is not possible.
|
||||
func UnwrapErrorWithFileContext(err error) *ErrorWithFileContext {
|
||||
for err != nil {
|
||||
switch v := err.(type) {
|
||||
case *ErrorWithFileContext:
|
||||
return v
|
||||
case causer:
|
||||
err = v.Cause()
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func chromaLexerFromType(fileType string) string {
|
||||
switch fileType {
|
||||
case "html", "htm":
|
||||
return "go-html-template"
|
||||
}
|
||||
return fileType
|
||||
}
|
||||
|
||||
func extNoDelimiter(filename string) string {
|
||||
return strings.TrimPrefix(filepath.Ext(filename), ".")
|
||||
}
|
||||
|
||||
func chromaLexerFromFilename(filename string) string {
|
||||
if strings.Contains(filename, "layouts") {
|
||||
return "go-html-template"
|
||||
}
|
||||
|
||||
ext := extNoDelimiter(filename)
|
||||
return chromaLexerFromType(ext)
|
||||
}
|
||||
|
||||
func locateErrorInString(src string, matcher LineMatcherFn) ErrorContext {
|
||||
return locateError(strings.NewReader(src), &fileError{}, matcher)
|
||||
}
|
||||
|
||||
func locateError(r io.Reader, le FileError, matches LineMatcherFn) ErrorContext {
|
||||
if le == nil {
|
||||
panic("must provide an error")
|
||||
}
|
||||
|
||||
errCtx := ErrorContext{position: text.Position{LineNumber: -1, ColumnNumber: 1, Offset: -1}, LinesPos: -1}
|
||||
|
||||
b, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
return errCtx
|
||||
}
|
||||
|
||||
pos := &errCtx.position
|
||||
lepos := le.Position()
|
||||
|
||||
lines := strings.Split(string(b), "\n")
|
||||
|
||||
if le != nil && lepos.ColumnNumber >= 0 {
|
||||
pos.ColumnNumber = lepos.ColumnNumber
|
||||
}
|
||||
|
||||
lineNo := 0
|
||||
posBytes := 0
|
||||
|
||||
for li, line := range lines {
|
||||
lineNo = li + 1
|
||||
m := LineMatcher{
|
||||
Position: le.Position(),
|
||||
Error: le,
|
||||
LineNumber: lineNo,
|
||||
Offset: posBytes,
|
||||
Line: line,
|
||||
}
|
||||
if errCtx.LinesPos == -1 && matches(m) {
|
||||
pos.LineNumber = lineNo
|
||||
break
|
||||
}
|
||||
|
||||
posBytes += len(line)
|
||||
}
|
||||
|
||||
if pos.LineNumber != -1 {
|
||||
low := pos.LineNumber - 3
|
||||
if low < 0 {
|
||||
low = 0
|
||||
}
|
||||
|
||||
if pos.LineNumber > 2 {
|
||||
errCtx.LinesPos = 2
|
||||
} else {
|
||||
errCtx.LinesPos = pos.LineNumber - 1
|
||||
}
|
||||
|
||||
high := pos.LineNumber + 2
|
||||
if high > len(lines) {
|
||||
high = len(lines)
|
||||
}
|
||||
|
||||
errCtx.Lines = lines[low:high]
|
||||
|
||||
}
|
||||
|
||||
return errCtx
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
// Copyright 2018 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 errors contains common Hugo errors and error related utilities.
|
||||
package herrors
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestErrorLocator(t *testing.T) {
|
||||
assert := require.New(t)
|
||||
|
||||
lineMatcher := func(m LineMatcher) bool {
|
||||
return strings.Contains(m.Line, "THEONE")
|
||||
}
|
||||
|
||||
lines := `LINE 1
|
||||
LINE 2
|
||||
LINE 3
|
||||
LINE 4
|
||||
This is THEONE
|
||||
LINE 6
|
||||
LINE 7
|
||||
LINE 8
|
||||
`
|
||||
|
||||
location := locateErrorInString(lines, lineMatcher)
|
||||
assert.Equal([]string{"LINE 3", "LINE 4", "This is THEONE", "LINE 6", "LINE 7"}, location.Lines)
|
||||
|
||||
pos := location.Position()
|
||||
assert.Equal(5, pos.LineNumber)
|
||||
assert.Equal(2, location.LinesPos)
|
||||
|
||||
assert.Equal([]string{"This is THEONE"}, locateErrorInString(`This is THEONE`, lineMatcher).Lines)
|
||||
|
||||
location = locateErrorInString(`L1
|
||||
This is THEONE
|
||||
L2
|
||||
`, lineMatcher)
|
||||
assert.Equal(2, location.Position().LineNumber)
|
||||
assert.Equal(1, location.LinesPos)
|
||||
assert.Equal([]string{"L1", "This is THEONE", "L2", ""}, location.Lines)
|
||||
|
||||
location = locateErrorInString(`This is THEONE
|
||||
L2
|
||||
`, lineMatcher)
|
||||
assert.Equal(0, location.LinesPos)
|
||||
assert.Equal([]string{"This is THEONE", "L2", ""}, location.Lines)
|
||||
|
||||
location = locateErrorInString(`L1
|
||||
This THEONE
|
||||
`, lineMatcher)
|
||||
assert.Equal([]string{"L1", "This THEONE", ""}, location.Lines)
|
||||
assert.Equal(1, location.LinesPos)
|
||||
|
||||
location = locateErrorInString(`L1
|
||||
L2
|
||||
This THEONE
|
||||
`, lineMatcher)
|
||||
assert.Equal([]string{"L1", "L2", "This THEONE", ""}, location.Lines)
|
||||
assert.Equal(2, location.LinesPos)
|
||||
|
||||
location = locateErrorInString("NO MATCH", lineMatcher)
|
||||
assert.Equal(-1, location.Position().LineNumber)
|
||||
assert.Equal(-1, location.LinesPos)
|
||||
assert.Equal(0, len(location.Lines))
|
||||
|
||||
lineMatcher = func(m LineMatcher) bool {
|
||||
return m.LineNumber == 6
|
||||
}
|
||||
|
||||
location = locateErrorInString(`A
|
||||
B
|
||||
C
|
||||
D
|
||||
E
|
||||
F
|
||||
G
|
||||
H
|
||||
I
|
||||
J`, lineMatcher)
|
||||
|
||||
assert.Equal([]string{"D", "E", "F", "G", "H"}, location.Lines)
|
||||
assert.Equal(6, location.Position().LineNumber)
|
||||
assert.Equal(2, location.LinesPos)
|
||||
|
||||
// Test match EOF
|
||||
lineMatcher = func(m LineMatcher) bool {
|
||||
return m.LineNumber == 4
|
||||
}
|
||||
|
||||
location = locateErrorInString(`A
|
||||
B
|
||||
C
|
||||
`, lineMatcher)
|
||||
|
||||
assert.Equal([]string{"B", "C", ""}, location.Lines)
|
||||
assert.Equal(4, location.Position().LineNumber)
|
||||
assert.Equal(2, location.LinesPos)
|
||||
|
||||
offsetMatcher := func(m LineMatcher) bool {
|
||||
return m.Offset == 1
|
||||
}
|
||||
|
||||
location = locateErrorInString(`A
|
||||
B
|
||||
C
|
||||
D
|
||||
E`, offsetMatcher)
|
||||
|
||||
assert.Equal([]string{"A", "B", "C", "D"}, location.Lines)
|
||||
assert.Equal(2, location.Position().LineNumber)
|
||||
assert.Equal(1, location.LinesPos)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// Copyright 2018 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 herrors contains common Hugo errors and error related utilities.
|
||||
package herrors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
|
||||
_errors "github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// As defined in https://godoc.org/github.com/pkg/errors
|
||||
type causer interface {
|
||||
Cause() error
|
||||
}
|
||||
|
||||
type stackTracer interface {
|
||||
StackTrace() _errors.StackTrace
|
||||
}
|
||||
|
||||
// PrintStackTrace prints the error's stack trace to stdoud.
|
||||
func PrintStackTrace(err error) {
|
||||
FprintStackTrace(os.Stdout, err)
|
||||
}
|
||||
|
||||
// FprintStackTrace prints the error's stack trace to w.
|
||||
func FprintStackTrace(w io.Writer, err error) {
|
||||
if err, ok := err.(stackTracer); ok {
|
||||
for _, f := range err.StackTrace() {
|
||||
fmt.Fprintf(w, "%+s:%d\n", f, f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recover is a helper function that can be used to capture panics.
|
||||
// Put this at the top of a method/function that crashes in a template:
|
||||
// defer herrors.Recover()
|
||||
func Recover() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Println("stacktrace from panic: \n" + string(debug.Stack()))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ErrFeatureNotAvailable denotes that a feature is unavailable.
|
||||
//
|
||||
// We will, at least to begin with, make some Hugo features (SCSS with libsass) optional,
|
||||
// and this error is used to signal those situations.
|
||||
var ErrFeatureNotAvailable = errors.New("this feature is not available in your current Hugo version, see https://goo.gl/YMrWcn for more information")
|
||||
@@ -0,0 +1,133 @@
|
||||
// Copyright 2018 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
|
||||
// limitatio ns under the License.
|
||||
|
||||
package herrors
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/gohugoio/hugo/common/text"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
_ causer = (*fileError)(nil)
|
||||
)
|
||||
|
||||
// FileError represents an error when handling a file: Parsing a config file,
|
||||
// execute a template etc.
|
||||
type FileError interface {
|
||||
error
|
||||
|
||||
text.Positioner
|
||||
|
||||
// A string identifying the type of file, e.g. JSON, TOML, markdown etc.
|
||||
Type() string
|
||||
}
|
||||
|
||||
var _ FileError = (*fileError)(nil)
|
||||
|
||||
type fileError struct {
|
||||
position text.Position
|
||||
|
||||
fileType string
|
||||
|
||||
cause error
|
||||
}
|
||||
|
||||
// Position returns the text position of this error.
|
||||
func (e fileError) Position() text.Position {
|
||||
return e.position
|
||||
}
|
||||
|
||||
func (e *fileError) Type() string {
|
||||
return e.fileType
|
||||
}
|
||||
|
||||
func (e *fileError) Error() string {
|
||||
if e.cause == nil {
|
||||
return ""
|
||||
}
|
||||
return e.cause.Error()
|
||||
}
|
||||
|
||||
func (f *fileError) Cause() error {
|
||||
return f.cause
|
||||
}
|
||||
|
||||
// NewFileError creates a new FileError.
|
||||
func NewFileError(fileType string, offset, lineNumber, columnNumber int, err error) FileError {
|
||||
pos := text.Position{Offset: offset, LineNumber: lineNumber, ColumnNumber: columnNumber}
|
||||
return &fileError{cause: err, fileType: fileType, position: pos}
|
||||
}
|
||||
|
||||
// UnwrapFileError tries to unwrap a FileError from err.
|
||||
// It returns nil if this is not possible.
|
||||
func UnwrapFileError(err error) FileError {
|
||||
for err != nil {
|
||||
switch v := err.(type) {
|
||||
case FileError:
|
||||
return v
|
||||
case causer:
|
||||
err = v.Cause()
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToFileErrorWithOffset will return a new FileError with a line number
|
||||
// with the given offset from the original.
|
||||
func ToFileErrorWithOffset(fe FileError, offset int) FileError {
|
||||
pos := fe.Position()
|
||||
return ToFileErrorWithLineNumber(fe, pos.LineNumber+offset)
|
||||
}
|
||||
|
||||
// ToFileErrorWithOffset will return a new FileError with the given line number.
|
||||
func ToFileErrorWithLineNumber(fe FileError, lineNumber int) FileError {
|
||||
pos := fe.Position()
|
||||
pos.LineNumber = lineNumber
|
||||
return &fileError{cause: fe, fileType: fe.Type(), position: pos}
|
||||
}
|
||||
|
||||
// ToFileError will convert the given error to an error supporting
|
||||
// the FileError interface.
|
||||
func ToFileError(fileType string, err error) FileError {
|
||||
for _, handle := range lineNumberExtractors {
|
||||
lno, col := handle(err)
|
||||
offset, typ := extractOffsetAndType(err)
|
||||
if fileType == "" {
|
||||
fileType = typ
|
||||
}
|
||||
|
||||
if lno > 0 || offset != -1 {
|
||||
return NewFileError(fileType, offset, lno, col, err)
|
||||
}
|
||||
}
|
||||
// Fall back to the pointing to line number 1.
|
||||
return NewFileError(fileType, -1, 1, 1, err)
|
||||
}
|
||||
|
||||
func extractOffsetAndType(e error) (int, string) {
|
||||
e = errors.Cause(e)
|
||||
switch v := e.(type) {
|
||||
case *json.UnmarshalTypeError:
|
||||
return int(v.Offset), "json"
|
||||
case *json.SyntaxError:
|
||||
return int(v.Offset), "json"
|
||||
default:
|
||||
return -1, ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright 2018 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 herrors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestToLineNumberError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert := require.New(t)
|
||||
|
||||
for i, test := range []struct {
|
||||
in error
|
||||
offset int
|
||||
lineNumber int
|
||||
columnNumber int
|
||||
}{
|
||||
{errors.New("no line number for you"), 0, 1, 1},
|
||||
{errors.New(`template: _default/single.html:4:15: executing "_default/single.html" at <.Titles>: can't evaluate field Titles in type *hugolib.PageOutput`), 0, 4, 15},
|
||||
{errors.New("parse failed: template: _default/bundle-resource-meta.html:11: unexpected in operand"), 0, 11, 1},
|
||||
{errors.New(`failed:: template: _default/bundle-resource-meta.html:2:7: executing "main" at <.Titles>`), 0, 2, 7},
|
||||
{errors.New("error in front matter: Near line 32 (last key parsed 'title')"), 0, 32, 1},
|
||||
{errors.New(`failed to load translations: (6, 7): was expecting token =, but got "g" instead`), 0, 6, 7},
|
||||
} {
|
||||
|
||||
got := ToFileError("template", test.in)
|
||||
|
||||
errMsg := fmt.Sprintf("[%d][%T]", i, got)
|
||||
le, ok := got.(FileError)
|
||||
assert.True(ok)
|
||||
|
||||
assert.True(ok, errMsg)
|
||||
pos := le.Position()
|
||||
assert.Equal(test.lineNumber, pos.LineNumber, errMsg)
|
||||
assert.Equal(test.columnNumber, pos.ColumnNumber, errMsg)
|
||||
assert.Error(errors.Cause(got))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// Copyright 2018 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
|
||||
// limitatio ns under the License.
|
||||
|
||||
package herrors
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var lineNumberExtractors = []lineNumberExtractor{
|
||||
// Template/shortcode parse errors
|
||||
newLineNumberErrHandlerFromRegexp(".*:(\\d+):(\\d*):"),
|
||||
newLineNumberErrHandlerFromRegexp(".*:(\\d+):"),
|
||||
|
||||
// TOML parse errors
|
||||
newLineNumberErrHandlerFromRegexp(".*Near line (\\d+)(\\s.*)"),
|
||||
|
||||
// YAML parse errors
|
||||
newLineNumberErrHandlerFromRegexp("line (\\d+):"),
|
||||
|
||||
// i18n bundle errors
|
||||
newLineNumberErrHandlerFromRegexp("\\((\\d+),\\s(\\d*)"),
|
||||
}
|
||||
|
||||
type lineNumberExtractor func(e error) (int, int)
|
||||
|
||||
func newLineNumberErrHandlerFromRegexp(expression string) lineNumberExtractor {
|
||||
re := regexp.MustCompile(expression)
|
||||
return extractLineNo(re)
|
||||
}
|
||||
|
||||
func extractLineNo(re *regexp.Regexp) lineNumberExtractor {
|
||||
return func(e error) (int, int) {
|
||||
if e == nil {
|
||||
panic("no error")
|
||||
}
|
||||
col := 1
|
||||
s := e.Error()
|
||||
m := re.FindStringSubmatch(s)
|
||||
if len(m) >= 2 {
|
||||
lno, _ := strconv.Atoi(m[1])
|
||||
if len(m) > 2 {
|
||||
col, _ = strconv.Atoi(m[2])
|
||||
}
|
||||
|
||||
if col <= 0 {
|
||||
col = 1
|
||||
}
|
||||
|
||||
return lno, col
|
||||
}
|
||||
|
||||
return -1, col
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
// Copyright 2019 The Hugo Authors. All rights reserved.
|
||||
// Some functions in this file (see comments) is based on the Go source code,
|
||||
// copyright The Go Authors and governed by a BSD-style license.
|
||||
//
|
||||
// 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 hreflect contains reflect helpers.
|
||||
package hreflect
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
)
|
||||
|
||||
// IsTruthful returns whether in represents a truthful value.
|
||||
// See IsTruthfulValue
|
||||
func IsTruthful(in interface{}) bool {
|
||||
switch v := in.(type) {
|
||||
case reflect.Value:
|
||||
return IsTruthfulValue(v)
|
||||
default:
|
||||
return IsTruthfulValue(reflect.ValueOf(in))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var zeroType = reflect.TypeOf((*types.Zeroer)(nil)).Elem()
|
||||
|
||||
// IsTruthfulValue returns whether the given value has a meaningful truth value.
|
||||
// This is based on template.IsTrue in Go's stdlib, but also considers
|
||||
// IsZero and any interface value will be unwrapped before it's considered
|
||||
// for truthfulness.
|
||||
//
|
||||
// Based on:
|
||||
// https://github.com/golang/go/blob/178a2c42254166cffed1b25fb1d3c7a5727cada6/src/text/template/exec.go#L306
|
||||
func IsTruthfulValue(val reflect.Value) (truth bool) {
|
||||
val = indirectInterface(val)
|
||||
|
||||
if !val.IsValid() {
|
||||
// Something like var x interface{}, never set. It's a form of nil.
|
||||
return
|
||||
}
|
||||
|
||||
if val.Type().Implements(zeroType) {
|
||||
return !val.Interface().(types.Zeroer).IsZero()
|
||||
}
|
||||
|
||||
switch val.Kind() {
|
||||
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
|
||||
truth = val.Len() > 0
|
||||
case reflect.Bool:
|
||||
truth = val.Bool()
|
||||
case reflect.Complex64, reflect.Complex128:
|
||||
truth = val.Complex() != 0
|
||||
case reflect.Chan, reflect.Func, reflect.Ptr, reflect.Interface:
|
||||
truth = !val.IsNil()
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
truth = val.Int() != 0
|
||||
case reflect.Float32, reflect.Float64:
|
||||
truth = val.Float() != 0
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
truth = val.Uint() != 0
|
||||
case reflect.Struct:
|
||||
truth = true // Struct values are always true.
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Based on: https://github.com/golang/go/blob/178a2c42254166cffed1b25fb1d3c7a5727cada6/src/text/template/exec.go#L931
|
||||
func indirectInterface(v reflect.Value) reflect.Value {
|
||||
if v.Kind() != reflect.Interface {
|
||||
return v
|
||||
}
|
||||
if v.IsNil() {
|
||||
return reflect.Value{}
|
||||
}
|
||||
return v.Elem()
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// 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.
|
||||
|
||||
package hreflect
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestIsTruthful(t *testing.T) {
|
||||
assert := require.New(t)
|
||||
|
||||
assert.True(IsTruthful(true))
|
||||
assert.False(IsTruthful(false))
|
||||
assert.True(IsTruthful(time.Now()))
|
||||
assert.False(IsTruthful(time.Time{}))
|
||||
}
|
||||
|
||||
func BenchmarkIsTruthFul(b *testing.B) {
|
||||
v := reflect.ValueOf("Hugo")
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if !IsTruthfulValue(v) {
|
||||
b.Fatal("not truthful")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// 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.
|
||||
|
||||
package hugio
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
// CopyFile copies a file.
|
||||
func CopyFile(fs afero.Fs, from, to string) error {
|
||||
sf, err := os.Open(from)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer sf.Close()
|
||||
df, err := os.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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CopyDir copies a directory.
|
||||
func CopyDir(fs afero.Fs, from, to string, shouldCopy func(filename string) bool) error {
|
||||
fi, err := os.Stat(from)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !fi.IsDir() {
|
||||
return errors.Errorf("%q is not a directory", from)
|
||||
}
|
||||
|
||||
err = fs.MkdirAll(to, 0777) // before umask
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
entries, _ := ioutil.ReadDir(from)
|
||||
for _, entry := range entries {
|
||||
fromFilename := filepath.Join(from, entry.Name())
|
||||
toFilename := filepath.Join(to, entry.Name())
|
||||
if entry.IsDir() {
|
||||
if shouldCopy != nil && !shouldCopy(fromFilename) {
|
||||
continue
|
||||
}
|
||||
if err := CopyDir(fs, fromFilename, toFilename, shouldCopy); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := CopyFile(fs, fromFilename, toFilename); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
// 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.
|
||||
@@ -11,7 +11,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package resource
|
||||
package hugio
|
||||
|
||||
import (
|
||||
"io"
|
||||
@@ -32,6 +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.
|
||||
type ReadSeekerNoOpCloser struct {
|
||||
ReadSeeker
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// Copyright 2018 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 hugio
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
type multiWriteCloser struct {
|
||||
io.Writer
|
||||
closers []io.WriteCloser
|
||||
}
|
||||
|
||||
func (m multiWriteCloser) Close() error {
|
||||
var err error
|
||||
for _, c := range m.closers {
|
||||
if closeErr := c.Close(); err != nil {
|
||||
err = closeErr
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// NewMultiWriteCloser creates a new io.WriteCloser that duplicates its writes to all the
|
||||
// provided writers.
|
||||
func NewMultiWriteCloser(writeClosers ...io.WriteCloser) io.WriteCloser {
|
||||
writers := make([]io.Writer, len(writeClosers))
|
||||
for i, w := range writeClosers {
|
||||
writers[i] = w
|
||||
}
|
||||
return multiWriteCloser{Writer: io.MultiWriter(writers...), closers: writeClosers}
|
||||
}
|
||||
|
||||
// ToWriteCloser creates an io.WriteCloser from the given io.Writer.
|
||||
// If it's not already, one will be created with a Close method that does nothing.
|
||||
func ToWriteCloser(w io.Writer) io.WriteCloser {
|
||||
if rw, ok := w.(io.WriteCloser); ok {
|
||||
return rw
|
||||
}
|
||||
|
||||
return struct {
|
||||
io.Writer
|
||||
io.Closer
|
||||
}{
|
||||
w,
|
||||
ioutil.NopCloser(nil),
|
||||
}
|
||||
}
|
||||
|
||||
// ToReadCloser creates an io.ReadCloser from the given io.Reader.
|
||||
// If it's not already, one will be created with a Close method that does nothing.
|
||||
func ToReadCloser(r io.Reader) io.ReadCloser {
|
||||
if rc, ok := r.(io.ReadCloser); ok {
|
||||
return rc
|
||||
}
|
||||
|
||||
return struct {
|
||||
io.Reader
|
||||
io.Closer
|
||||
}{
|
||||
r,
|
||||
ioutil.NopCloser(nil),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// Copyright 2018 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 hugo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
)
|
||||
|
||||
const (
|
||||
EnvironmentDevelopment = "development"
|
||||
EnvironmentProduction = "production"
|
||||
)
|
||||
|
||||
var (
|
||||
// commitHash contains the current Git revision. Use make to build to make
|
||||
// sure this gets set.
|
||||
commitHash string
|
||||
|
||||
// buildDate contains the date of the current build.
|
||||
buildDate string
|
||||
)
|
||||
|
||||
// Info contains information about the current Hugo environment
|
||||
type Info struct {
|
||||
CommitHash string
|
||||
BuildDate string
|
||||
|
||||
// The build environment.
|
||||
// Defaults are "production" (hugo) and "development" (hugo server).
|
||||
// This can also be set by the user.
|
||||
// It can be any string, but it will be all lower case.
|
||||
Environment string
|
||||
}
|
||||
|
||||
// Version returns the current version as a comparable version string.
|
||||
func (i Info) Version() VersionString {
|
||||
return CurrentVersion.Version()
|
||||
}
|
||||
|
||||
// Generator a Hugo meta generator HTML tag.
|
||||
func (i Info) Generator() template.HTML {
|
||||
return template.HTML(fmt.Sprintf(`<meta name="generator" content="Hugo %s" />`, CurrentVersion.String()))
|
||||
}
|
||||
|
||||
// NewInfo creates a new Hugo Info object.
|
||||
func NewInfo(environment string) Info {
|
||||
if environment == "" {
|
||||
environment = EnvironmentProduction
|
||||
}
|
||||
return Info{
|
||||
CommitHash: commitHash,
|
||||
BuildDate: buildDate,
|
||||
Environment: environment,
|
||||
}
|
||||
}
|
||||
@@ -11,23 +11,25 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package hugolib
|
||||
package hugo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestHugoInfo(t *testing.T) {
|
||||
assert := require.New(t)
|
||||
|
||||
assert.Equal(helpers.CurrentHugoVersion.Version(), hugoInfo.Version)
|
||||
assert.IsType(helpers.HugoVersionString(""), hugoInfo.Version)
|
||||
assert.Equal(CommitHash, hugoInfo.CommitHash)
|
||||
assert.Equal(BuildDate, hugoInfo.BuildDate)
|
||||
assert.Contains(hugoInfo.Generator, fmt.Sprintf("Hugo %s", hugoInfo.Version))
|
||||
hugoInfo := NewInfo("")
|
||||
|
||||
assert.Equal(CurrentVersion.Version(), hugoInfo.Version())
|
||||
assert.IsType(VersionString(""), hugoInfo.Version())
|
||||
assert.Equal(commitHash, hugoInfo.CommitHash)
|
||||
assert.Equal(buildDate, hugoInfo.BuildDate)
|
||||
assert.Equal("production", hugoInfo.Environment)
|
||||
assert.Contains(hugoInfo.Generator(), fmt.Sprintf("Hugo %s", hugoInfo.Version()))
|
||||
|
||||
}
|
||||
@@ -11,13 +11,8 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// +build windows
|
||||
// +build extended
|
||||
|
||||
package commands
|
||||
package hugo
|
||||
|
||||
const (
|
||||
ansiEsc = ""
|
||||
clearLine = ""
|
||||
hideCursor = ""
|
||||
showCursor = ""
|
||||
)
|
||||
var IsExtended = true
|
||||
@@ -11,13 +11,8 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// +build !windows
|
||||
// +build !extended
|
||||
|
||||
package commands
|
||||
package hugo
|
||||
|
||||
const (
|
||||
ansiEsc = "\u001B"
|
||||
clearLine = "\r\033[K"
|
||||
hideCursor = ansiEsc + "[?25l"
|
||||
showCursor = ansiEsc + "[?25h"
|
||||
)
|
||||
var IsExtended = false
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2015 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2018 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.
|
||||
@@ -11,18 +11,21 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package helpers
|
||||
package hugo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/compare"
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
// HugoVersion represents the Hugo build version.
|
||||
type HugoVersion struct {
|
||||
// Version represents the Hugo build version.
|
||||
type Version struct {
|
||||
// Major and minor version.
|
||||
Number float32
|
||||
|
||||
@@ -35,32 +38,34 @@ type HugoVersion struct {
|
||||
}
|
||||
|
||||
var (
|
||||
_ compare.Eqer = (*HugoVersionString)(nil)
|
||||
_ compare.Comparer = (*HugoVersionString)(nil)
|
||||
_ compare.Eqer = (*VersionString)(nil)
|
||||
_ compare.Comparer = (*VersionString)(nil)
|
||||
)
|
||||
|
||||
type HugoVersionString string
|
||||
|
||||
func (v HugoVersion) String() string {
|
||||
return hugoVersion(v.Number, v.PatchLevel, v.Suffix)
|
||||
func (v Version) String() string {
|
||||
return version(v.Number, v.PatchLevel, v.Suffix)
|
||||
}
|
||||
|
||||
func (v HugoVersion) Version() HugoVersionString {
|
||||
return HugoVersionString(v.String())
|
||||
// Version returns the Hugo version.
|
||||
func (v Version) Version() VersionString {
|
||||
return VersionString(v.String())
|
||||
}
|
||||
|
||||
func (h HugoVersionString) String() string {
|
||||
// VersionString represents a Hugo version string.
|
||||
type VersionString string
|
||||
|
||||
func (h VersionString) String() string {
|
||||
return string(h)
|
||||
}
|
||||
|
||||
// Implements compare.Comparer
|
||||
func (h HugoVersionString) Compare(other interface{}) int {
|
||||
v := MustParseHugoVersion(h.String())
|
||||
// Compare implements the compare.Comparer interface.
|
||||
func (h VersionString) Compare(other interface{}) int {
|
||||
v := MustParseVersion(h.String())
|
||||
return compareVersionsWithSuffix(v.Number, v.PatchLevel, v.Suffix, other)
|
||||
}
|
||||
|
||||
// Implements compare.Eqer
|
||||
func (h HugoVersionString) Eq(other interface{}) bool {
|
||||
// Eq implements the compare.Eqer interface.
|
||||
func (h VersionString) Eq(other interface{}) bool {
|
||||
s, err := cast.ToStringE(other)
|
||||
if err != nil {
|
||||
return false
|
||||
@@ -70,9 +75,9 @@ func (h HugoVersionString) Eq(other interface{}) bool {
|
||||
|
||||
var versionSuffixes = []string{"-test", "-DEV"}
|
||||
|
||||
// ParseHugoVersion parses a version string.
|
||||
func ParseHugoVersion(s string) (HugoVersion, error) {
|
||||
var vv HugoVersion
|
||||
// ParseVersion parses a version string.
|
||||
func ParseVersion(s string) (Version, error) {
|
||||
var vv Version
|
||||
for _, suffix := range versionSuffixes {
|
||||
if strings.HasSuffix(s, suffix) {
|
||||
vv.Suffix = suffix
|
||||
@@ -88,10 +93,10 @@ func ParseHugoVersion(s string) (HugoVersion, error) {
|
||||
return vv, nil
|
||||
}
|
||||
|
||||
// MustParseHugoVersion parses a version string
|
||||
// MustParseVersion parses a version string
|
||||
// and panics if any error occurs.
|
||||
func MustParseHugoVersion(s string) HugoVersion {
|
||||
vv, err := ParseHugoVersion(s)
|
||||
func MustParseVersion(s string) Version {
|
||||
vv, err := ParseVersion(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -99,37 +104,53 @@ func MustParseHugoVersion(s string) HugoVersion {
|
||||
}
|
||||
|
||||
// ReleaseVersion represents the release version.
|
||||
func (v HugoVersion) ReleaseVersion() HugoVersion {
|
||||
func (v Version) ReleaseVersion() Version {
|
||||
v.Suffix = ""
|
||||
return v
|
||||
}
|
||||
|
||||
// Next returns the next Hugo release version.
|
||||
func (v HugoVersion) Next() HugoVersion {
|
||||
return HugoVersion{Number: v.Number + 0.01}
|
||||
func (v Version) Next() Version {
|
||||
return Version{Number: v.Number + 0.01}
|
||||
}
|
||||
|
||||
// Prev returns the previous Hugo release version.
|
||||
func (v HugoVersion) Prev() HugoVersion {
|
||||
return HugoVersion{Number: v.Number - 0.01}
|
||||
func (v Version) Prev() Version {
|
||||
return Version{Number: v.Number - 0.01}
|
||||
}
|
||||
|
||||
// NextPatchLevel returns the next patch/bugfix Hugo version.
|
||||
// This will be a patch increment on the previous Hugo version.
|
||||
func (v HugoVersion) NextPatchLevel(level int) HugoVersion {
|
||||
return HugoVersion{Number: v.Number - 0.01, PatchLevel: level}
|
||||
func (v Version) NextPatchLevel(level int) Version {
|
||||
return Version{Number: v.Number - 0.01, PatchLevel: level}
|
||||
}
|
||||
|
||||
// CurrentHugoVersion represents the current build version.
|
||||
// This should be the only one.
|
||||
var CurrentHugoVersion = HugoVersion{
|
||||
Number: 0.45,
|
||||
PatchLevel: 0,
|
||||
Suffix: "",
|
||||
// BuildVersionString creates a version string. This is what you see when
|
||||
// running "hugo version".
|
||||
func BuildVersionString() string {
|
||||
program := "Hugo Static Site Generator"
|
||||
|
||||
version := "v" + CurrentVersion.String()
|
||||
if commitHash != "" {
|
||||
version += "-" + strings.ToUpper(commitHash)
|
||||
}
|
||||
if IsExtended {
|
||||
version += "/extended"
|
||||
}
|
||||
|
||||
osArch := runtime.GOOS + "/" + runtime.GOARCH
|
||||
|
||||
date := buildDate
|
||||
if date == "" {
|
||||
date = "unknown"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s %s %s BuildDate: %s", program, version, osArch, date)
|
||||
|
||||
}
|
||||
|
||||
func hugoVersion(version float32, patchVersion int, suffix string) string {
|
||||
if patchVersion > 0 {
|
||||
func version(version float32, patchVersion int, suffix string) string {
|
||||
if patchVersion > 0 || version > 0.53 {
|
||||
return fmt.Sprintf("%.2f.%d%s", version, patchVersion, suffix)
|
||||
}
|
||||
return fmt.Sprintf("%.2f%s", version, suffix)
|
||||
@@ -140,7 +161,7 @@ func hugoVersion(version float32, patchVersion int, suffix string) string {
|
||||
// It returns -1 if the given version is less than, 0 if equal and 1 if greater than
|
||||
// the running version.
|
||||
func CompareVersion(version interface{}) int {
|
||||
return compareVersionsWithSuffix(CurrentHugoVersion.Number, CurrentHugoVersion.PatchLevel, CurrentHugoVersion.Suffix, version)
|
||||
return compareVersionsWithSuffix(CurrentVersion.Number, CurrentVersion.PatchLevel, CurrentVersion.Suffix, version)
|
||||
}
|
||||
|
||||
func compareVersions(inVersion float32, inPatchVersion int, in interface{}) int {
|
||||
@@ -166,7 +187,7 @@ func compareVersionsWithSuffix(inVersion float32, inPatchVersion int, suffix str
|
||||
return -1
|
||||
}
|
||||
|
||||
v, err := ParseHugoVersion(s)
|
||||
v, err := ParseVersion(s)
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
@@ -215,3 +236,16 @@ func compareFloatVersions(version float32, v float32) int {
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
func GoMinorVersion() int {
|
||||
return goMinorVersion(runtime.Version())
|
||||
}
|
||||
|
||||
func goMinorVersion(version string) int {
|
||||
if strings.HasPrefix(version, "devel") {
|
||||
return 9999 // magic
|
||||
}
|
||||
i, _ := strconv.Atoi(strings.Split(version, ".")[1])
|
||||
return i
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright 2018 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 hugo
|
||||
|
||||
// CurrentVersion represents the current build version.
|
||||
// This should be the only one.
|
||||
var CurrentVersion = Version{
|
||||
Number: 0.56,
|
||||
PatchLevel: 0,
|
||||
Suffix: "",
|
||||
}
|
||||
@@ -11,7 +11,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package helpers
|
||||
package hugo
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -21,10 +21,10 @@ import (
|
||||
)
|
||||
|
||||
func TestHugoVersion(t *testing.T) {
|
||||
assert.Equal(t, "0.15-DEV", hugoVersion(0.15, 0, "-DEV"))
|
||||
assert.Equal(t, "0.15.2-DEV", hugoVersion(0.15, 2, "-DEV"))
|
||||
assert.Equal(t, "0.15-DEV", version(0.15, 0, "-DEV"))
|
||||
assert.Equal(t, "0.15.2-DEV", version(0.15, 2, "-DEV"))
|
||||
|
||||
v := HugoVersion{Number: 0.21, PatchLevel: 0, Suffix: "-DEV"}
|
||||
v := Version{Number: 0.21, PatchLevel: 0, Suffix: "-DEV"}
|
||||
|
||||
require.Equal(t, v.ReleaseVersion().String(), "0.21")
|
||||
require.Equal(t, "0.21-DEV", v.String())
|
||||
@@ -35,6 +35,15 @@ func TestHugoVersion(t *testing.T) {
|
||||
require.False(t, nextVersionString.Eq("0.21"))
|
||||
require.True(t, nextVersionString.Eq(nextVersionString))
|
||||
require.Equal(t, "0.20.3", v.NextPatchLevel(3).String())
|
||||
|
||||
// We started to use full semver versions even for main
|
||||
// releases in v0.54.0
|
||||
v = Version{Number: 0.53, PatchLevel: 0}
|
||||
require.Equal(t, "0.53", v.String())
|
||||
require.Equal(t, "0.54.0", v.Next().String())
|
||||
require.Equal(t, "0.55.0", v.Next().Next().String())
|
||||
v = Version{Number: 0.54, PatchLevel: 0, Suffix: "-DEV"}
|
||||
require.Equal(t, "0.54.0-DEV", v.String())
|
||||
}
|
||||
|
||||
func TestCompareVersions(t *testing.T) {
|
||||
@@ -62,9 +71,15 @@ func TestCompareVersions(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseHugoVersion(t *testing.T) {
|
||||
require.Equal(t, "0.25", MustParseHugoVersion("0.25").String())
|
||||
require.Equal(t, "0.25.2", MustParseHugoVersion("0.25.2").String())
|
||||
require.Equal(t, "0.25-test", MustParseHugoVersion("0.25-test").String())
|
||||
require.Equal(t, "0.25-DEV", MustParseHugoVersion("0.25-DEV").String())
|
||||
require.Equal(t, "0.25", MustParseVersion("0.25").String())
|
||||
require.Equal(t, "0.25.2", MustParseVersion("0.25.2").String())
|
||||
require.Equal(t, "0.25-test", MustParseVersion("0.25-test").String())
|
||||
require.Equal(t, "0.25-DEV", MustParseVersion("0.25-DEV").String())
|
||||
|
||||
}
|
||||
|
||||
func TestGoMinorVersion(t *testing.T) {
|
||||
assert := require.New(t)
|
||||
assert.Equal(12, goMinorVersion("go1.12.5"))
|
||||
assert.True(GoMinorVersion() >= 11)
|
||||
}
|
||||
+145
-6
@@ -14,24 +14,163 @@
|
||||
package loggers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"regexp"
|
||||
|
||||
"github.com/gohugoio/hugo/common/terminal"
|
||||
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
)
|
||||
|
||||
var (
|
||||
// Counts ERROR logs to the global jww logger.
|
||||
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 {
|
||||
*jww.Notepad
|
||||
ErrorCounter *jww.Counter
|
||||
WarnCounter *jww.Counter
|
||||
|
||||
// This is only set in server mode.
|
||||
errors *bytes.Buffer
|
||||
}
|
||||
|
||||
func (l *Logger) Errors() string {
|
||||
if l.errors == nil {
|
||||
return ""
|
||||
}
|
||||
return ansiColorRe.ReplaceAllString(l.errors.String(), "")
|
||||
}
|
||||
|
||||
// Reset resets the logger's internal state.
|
||||
func (l *Logger) Reset() {
|
||||
l.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 {
|
||||
return newLogger(stdoutThreshold, logThreshold, outHandle, logHandle, saveErrors)
|
||||
}
|
||||
|
||||
// NewDebugLogger is a convenience function to create a debug logger.
|
||||
func NewDebugLogger() *jww.Notepad {
|
||||
return jww.NewNotepad(jww.LevelDebug, jww.LevelError, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime)
|
||||
func NewDebugLogger() *Logger {
|
||||
return newBasicLogger(jww.LevelDebug)
|
||||
}
|
||||
|
||||
// NewWarningLogger is a convenience function to create a warning logger.
|
||||
func NewWarningLogger() *jww.Notepad {
|
||||
return jww.NewNotepad(jww.LevelWarn, jww.LevelError, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime)
|
||||
func NewWarningLogger() *Logger {
|
||||
return newBasicLogger(jww.LevelWarn)
|
||||
}
|
||||
|
||||
// NewErrorLogger is a convenience function to create an error logger.
|
||||
func NewErrorLogger() *jww.Notepad {
|
||||
return jww.NewNotepad(jww.LevelError, jww.LevelError, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime)
|
||||
func NewErrorLogger() *Logger {
|
||||
return newBasicLogger(jww.LevelError)
|
||||
}
|
||||
|
||||
var (
|
||||
ansiColorRe = regexp.MustCompile("(?s)\\033\\[\\d*(;\\d*)*m")
|
||||
errorRe = regexp.MustCompile("^(ERROR|FATAL|WARN)")
|
||||
)
|
||||
|
||||
type ansiCleaner struct {
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
func (a ansiCleaner) Write(p []byte) (n int, err error) {
|
||||
return a.w.Write(ansiColorRe.ReplaceAll(p, []byte("")))
|
||||
}
|
||||
|
||||
type labelColorizer struct {
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
func (a labelColorizer) Write(p []byte) (n int, err error) {
|
||||
replaced := errorRe.ReplaceAllStringFunc(string(p), func(m string) string {
|
||||
switch m {
|
||||
case "ERROR", "FATAL":
|
||||
return terminal.Error(m)
|
||||
case "WARN":
|
||||
return terminal.Warning(m)
|
||||
default:
|
||||
return m
|
||||
}
|
||||
})
|
||||
// io.MultiWriter will abort if we return a bigger write count than input
|
||||
// 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.
|
||||
func InitGlobalLogger(stdoutThreshold, logThreshold jww.Threshold, outHandle, logHandle io.Writer) {
|
||||
outHandle, logHandle = getLogWriters(outHandle, logHandle)
|
||||
|
||||
jww.SetStdoutOutput(outHandle)
|
||||
jww.SetLogOutput(logHandle)
|
||||
jww.SetLogThreshold(logThreshold)
|
||||
jww.SetStdoutThreshold(stdoutThreshold)
|
||||
|
||||
}
|
||||
|
||||
func getLogWriters(outHandle, logHandle io.Writer) (io.Writer, io.Writer) {
|
||||
isTerm := terminal.IsTerminal(os.Stdout)
|
||||
if logHandle != ioutil.Discard && isTerm {
|
||||
// Remove any Ansi coloring from log output
|
||||
logHandle = ansiCleaner{w: logHandle}
|
||||
}
|
||||
|
||||
if isTerm {
|
||||
outHandle = labelColorizer{w: outHandle}
|
||||
}
|
||||
|
||||
return outHandle, logHandle
|
||||
|
||||
}
|
||||
|
||||
func newLogger(stdoutThreshold, logThreshold jww.Threshold, outHandle, logHandle io.Writer, saveErrors bool) *Logger {
|
||||
errorCounter := &jww.Counter{}
|
||||
warnCounter := &jww.Counter{}
|
||||
outHandle, logHandle = getLogWriters(outHandle, logHandle)
|
||||
|
||||
listeners := []jww.LogListener{jww.LogCounter(errorCounter, jww.LevelError), jww.LogCounter(warnCounter, jww.LevelWarn)}
|
||||
var errorBuff *bytes.Buffer
|
||||
if saveErrors {
|
||||
errorBuff = new(bytes.Buffer)
|
||||
errorCapture := func(t jww.Threshold) io.Writer {
|
||||
if t != jww.LevelError {
|
||||
// Only interested in ERROR
|
||||
return nil
|
||||
}
|
||||
|
||||
return errorBuff
|
||||
}
|
||||
|
||||
listeners = append(listeners, errorCapture)
|
||||
}
|
||||
|
||||
return &Logger{
|
||||
Notepad: jww.NewNotepad(stdoutThreshold, logThreshold, outHandle, logHandle, "", log.Ldate|log.Ltime, listeners...),
|
||||
ErrorCounter: errorCounter,
|
||||
WarnCounter: warnCounter,
|
||||
errors: errorBuff,
|
||||
}
|
||||
}
|
||||
|
||||
func newBasicLogger(t jww.Threshold) *Logger {
|
||||
return newLogger(t, jww.LevelError, os.Stdout, ioutil.Discard, false)
|
||||
}
|
||||
|
||||
@@ -11,13 +11,22 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package errors contains common Hugo errors and error related utilities.
|
||||
package errors
|
||||
package loggers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// We will, at least to begin with, make some Hugo features (SCSS with libsass) optional,
|
||||
// and this error is used to signal those situations.
|
||||
var FeatureNotAvailableErr = errors.New("this feature is not available in your current Hugo version")
|
||||
func TestLogger(t *testing.T) {
|
||||
assert := require.New(t)
|
||||
l := NewWarningLogger()
|
||||
|
||||
l.ERROR.Println("One error")
|
||||
l.ERROR.Println("Two error")
|
||||
l.WARN.Println("A warning")
|
||||
|
||||
assert.Equal(uint64(2), l.ErrorCounter.Count())
|
||||
|
||||
}
|
||||
@@ -16,6 +16,8 @@ package maps
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/gobwas/glob"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
@@ -42,3 +44,73 @@ func ToLower(m map[string]interface{}) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
type keyRename struct {
|
||||
pattern glob.Glob
|
||||
newKey string
|
||||
}
|
||||
|
||||
// KeyRenamer supports renaming of keys in a map.
|
||||
type KeyRenamer struct {
|
||||
renames []keyRename
|
||||
}
|
||||
|
||||
// NewKeyRenamer creates a new KeyRenamer given a list of pattern and new key
|
||||
// value pairs.
|
||||
func NewKeyRenamer(patternKeys ...string) (KeyRenamer, error) {
|
||||
var renames []keyRename
|
||||
for i := 0; i < len(patternKeys); i += 2 {
|
||||
g, err := glob.Compile(strings.ToLower(patternKeys[i]), '/')
|
||||
if err != nil {
|
||||
return KeyRenamer{}, err
|
||||
}
|
||||
renames = append(renames, keyRename{pattern: g, newKey: patternKeys[i+1]})
|
||||
}
|
||||
|
||||
return KeyRenamer{renames: renames}, nil
|
||||
}
|
||||
|
||||
func (r KeyRenamer) getNewKey(keyPath string) string {
|
||||
for _, matcher := range r.renames {
|
||||
if matcher.pattern.Match(keyPath) {
|
||||
return matcher.newKey
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// Rename renames the keys in the given map according
|
||||
// to the patterns in the current KeyRenamer.
|
||||
func (r KeyRenamer) Rename(m map[string]interface{}) {
|
||||
r.renamePath("", m)
|
||||
}
|
||||
|
||||
func (KeyRenamer) keyPath(k1, k2 string) string {
|
||||
k1, k2 = strings.ToLower(k1), strings.ToLower(k2)
|
||||
if k1 == "" {
|
||||
return k2
|
||||
} else {
|
||||
return k1 + "/" + k2
|
||||
}
|
||||
}
|
||||
|
||||
func (r KeyRenamer) renamePath(parentKeyPath string, m map[string]interface{}) {
|
||||
for key, val := range m {
|
||||
keyPath := r.keyPath(parentKeyPath, key)
|
||||
switch val.(type) {
|
||||
case map[interface{}]interface{}:
|
||||
val = cast.ToStringMap(val)
|
||||
r.renamePath(keyPath, val.(map[string]interface{}))
|
||||
case map[string]interface{}:
|
||||
r.renamePath(keyPath, val.(map[string]interface{}))
|
||||
}
|
||||
|
||||
newKey := r.getNewKey(keyPath)
|
||||
|
||||
if newKey != "" {
|
||||
delete(m, key)
|
||||
m[newKey] = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ package maps
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestToLower(t *testing.T) {
|
||||
@@ -70,3 +72,52 @@ func TestToLower(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenameKeys(t *testing.T) {
|
||||
assert := require.New(t)
|
||||
|
||||
m := map[string]interface{}{
|
||||
"a": 32,
|
||||
"ren1": "m1",
|
||||
"ren2": "m1_2",
|
||||
"sub": map[string]interface{}{
|
||||
"subsub": map[string]interface{}{
|
||||
"REN1": "m2",
|
||||
"ren2": "m2_2",
|
||||
},
|
||||
},
|
||||
"no": map[string]interface{}{
|
||||
"ren1": "m2",
|
||||
"ren2": "m2_2",
|
||||
},
|
||||
}
|
||||
|
||||
expected := map[string]interface{}{
|
||||
"a": 32,
|
||||
"new1": "m1",
|
||||
"new2": "m1_2",
|
||||
"sub": map[string]interface{}{
|
||||
"subsub": map[string]interface{}{
|
||||
"new1": "m2",
|
||||
"ren2": "m2_2",
|
||||
},
|
||||
},
|
||||
"no": map[string]interface{}{
|
||||
"ren1": "m2",
|
||||
"ren2": "m2_2",
|
||||
},
|
||||
}
|
||||
|
||||
renamer, err := NewKeyRenamer(
|
||||
"{ren1,sub/*/ren1}", "new1",
|
||||
"{Ren2,sub/ren2}", "new2",
|
||||
)
|
||||
assert.NoError(err)
|
||||
|
||||
renamer.Rename(m)
|
||||
|
||||
if !reflect.DeepEqual(expected, m) {
|
||||
t.Errorf("Expected\n%#v, got\n%#v\n", expected, m)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
// 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.
|
||||
|
||||
package maps
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
// GetNestedParam gets the first match of the keyStr in the candidates given.
|
||||
// It will first try the exact match and then try to find it as a nested map value,
|
||||
// using the given separator, e.g. "mymap.name".
|
||||
// It assumes that all the maps given have lower cased keys.
|
||||
func GetNestedParam(keyStr, separator string, candidates ...map[string]interface{}) (interface{}, error) {
|
||||
keyStr = strings.ToLower(keyStr)
|
||||
|
||||
lookupFn := func(key string) interface{} {
|
||||
for _, m := range candidates {
|
||||
if v, ok := m[key]; ok {
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
v, _, _, err := GetNestedParamFn(keyStr, separator, lookupFn)
|
||||
return v, err
|
||||
}
|
||||
|
||||
func GetNestedParamFn(keyStr, separator string, lookupFn func(key string) interface{}) (interface{}, string, map[string]interface{}, error) {
|
||||
result, _ := traverseDirectParams(keyStr, lookupFn)
|
||||
if result != nil {
|
||||
return result, keyStr, nil, nil
|
||||
}
|
||||
|
||||
keySegments := strings.Split(keyStr, separator)
|
||||
if len(keySegments) == 1 {
|
||||
return nil, keyStr, nil, nil
|
||||
}
|
||||
|
||||
return traverseNestedParams(keySegments, lookupFn)
|
||||
}
|
||||
|
||||
func traverseDirectParams(keyStr string, lookupFn func(key string) interface{}) (interface{}, error) {
|
||||
return lookupFn(keyStr), nil
|
||||
}
|
||||
|
||||
func traverseNestedParams(keySegments []string, lookupFn func(key string) interface{}) (interface{}, string, map[string]interface{}, error) {
|
||||
firstKey, rest := keySegments[0], keySegments[1:]
|
||||
result := lookupFn(firstKey)
|
||||
if result == nil || len(rest) == 0 {
|
||||
return result, firstKey, nil, nil
|
||||
}
|
||||
|
||||
switch m := result.(type) {
|
||||
case map[string]interface{}:
|
||||
v, key, owner := traverseParams(rest, m)
|
||||
return v, key, owner, nil
|
||||
default:
|
||||
return nil, "", nil, errors.Errorf("unsupported Params type: %T", result)
|
||||
}
|
||||
}
|
||||
|
||||
func traverseParams(keys []string, m map[string]interface{}) (interface{}, string, map[string]interface{}) {
|
||||
// Shift first element off.
|
||||
firstKey, rest := keys[0], keys[1:]
|
||||
result := m[firstKey]
|
||||
|
||||
// No point in continuing here.
|
||||
if result == nil {
|
||||
return result, "", nil
|
||||
}
|
||||
|
||||
if len(rest) == 0 {
|
||||
// That was the last key.
|
||||
return result, firstKey, m
|
||||
}
|
||||
|
||||
// That was not the last key.
|
||||
return traverseParams(rest, cast.ToStringMap(result))
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// 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.
|
||||
|
||||
package maps
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetNestedParam(t *testing.T) {
|
||||
|
||||
m := map[string]interface{}{
|
||||
"first": 1,
|
||||
"with_underscore": 2,
|
||||
"nested": map[string]interface{}{
|
||||
"color": "blue",
|
||||
},
|
||||
}
|
||||
|
||||
assert := require.New(t)
|
||||
|
||||
must := func(keyStr, separator string, candidates ...map[string]interface{}) interface{} {
|
||||
v, err := GetNestedParam(keyStr, separator, candidates...)
|
||||
assert.NoError(err)
|
||||
return v
|
||||
}
|
||||
|
||||
assert.Equal(1, must("first", "_", m))
|
||||
assert.Equal(1, must("First", "_", m))
|
||||
assert.Equal(2, must("with_underscore", "_", m))
|
||||
assert.Equal("blue", must("nested_color", "_", m))
|
||||
|
||||
}
|
||||
+28
-10
@@ -1,4 +1,4 @@
|
||||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
// 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.
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/gohugoio/hugo/common/collections"
|
||||
"github.com/gohugoio/hugo/common/math"
|
||||
)
|
||||
|
||||
@@ -27,6 +28,24 @@ type Scratch struct {
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// Scratcher provides a scratching service.
|
||||
type Scratcher interface {
|
||||
Scratch() *Scratch
|
||||
}
|
||||
|
||||
type scratcher struct {
|
||||
s *Scratch
|
||||
}
|
||||
|
||||
func (s scratcher) Scratch() *Scratch {
|
||||
return s.s
|
||||
}
|
||||
|
||||
// NewScratcher creates a new Scratcher.
|
||||
func NewScratcher() Scratcher {
|
||||
return scratcher{s: NewScratch()}
|
||||
}
|
||||
|
||||
// Add will, for single values, add (using the + operator) the addend to the existing addend (if found).
|
||||
// Supports numeric values and strings.
|
||||
//
|
||||
@@ -40,14 +59,12 @@ func (c *Scratch) Add(key string, newAddend interface{}) (string, error) {
|
||||
if found {
|
||||
var err error
|
||||
|
||||
addendV := reflect.ValueOf(existingAddend)
|
||||
addendV := reflect.TypeOf(existingAddend)
|
||||
|
||||
if addendV.Kind() == reflect.Slice || addendV.Kind() == reflect.Array {
|
||||
nav := reflect.ValueOf(newAddend)
|
||||
if nav.Kind() == reflect.Slice || nav.Kind() == reflect.Array {
|
||||
newVal = reflect.AppendSlice(addendV, nav).Interface()
|
||||
} else {
|
||||
newVal = reflect.Append(addendV, nav).Interface()
|
||||
newVal, err = collections.Append(existingAddend, newAddend)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
} else {
|
||||
newVal, err = math.DoArithmetic(existingAddend, newAddend, '+')
|
||||
@@ -73,7 +90,7 @@ func (c *Scratch) Set(key string, value interface{}) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Reset deletes the given key
|
||||
// Delete deletes the given key.
|
||||
func (c *Scratch) Delete(key string) string {
|
||||
c.mu.Lock()
|
||||
delete(c.values, key)
|
||||
@@ -81,7 +98,7 @@ func (c *Scratch) Delete(key string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Get returns a value previously set by Add or Set
|
||||
// Get returns a value previously set by Add or Set.
|
||||
func (c *Scratch) Get(key string) interface{} {
|
||||
c.mu.RLock()
|
||||
val := c.values[key]
|
||||
@@ -104,7 +121,7 @@ func (c *Scratch) SetInMap(key string, mapKey string, value interface{}) string
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetSortedMapValues returns a sorted map previously filled with SetInMap
|
||||
// GetSortedMapValues returns a sorted map previously filled with SetInMap.
|
||||
func (c *Scratch) GetSortedMapValues(key string) interface{} {
|
||||
c.mu.RLock()
|
||||
|
||||
@@ -130,6 +147,7 @@ func (c *Scratch) GetSortedMapValues(key string) interface{} {
|
||||
return sortedArray
|
||||
}
|
||||
|
||||
// NewScratch returns a new instance Scratch.
|
||||
func NewScratch() *Scratch {
|
||||
return &Scratch{values: make(map[string]interface{})}
|
||||
}
|
||||
|
||||
+49
-12
@@ -18,29 +18,31 @@ import (
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestScratchAdd(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert := require.New(t)
|
||||
|
||||
scratch := NewScratch()
|
||||
scratch.Add("int1", 10)
|
||||
scratch.Add("int1", 20)
|
||||
scratch.Add("int2", 20)
|
||||
|
||||
assert.Equal(t, int64(30), scratch.Get("int1"))
|
||||
assert.Equal(t, 20, scratch.Get("int2"))
|
||||
assert.Equal(int64(30), scratch.Get("int1"))
|
||||
assert.Equal(20, scratch.Get("int2"))
|
||||
|
||||
scratch.Add("float1", float64(10.5))
|
||||
scratch.Add("float1", float64(20.1))
|
||||
|
||||
assert.Equal(t, float64(30.6), scratch.Get("float1"))
|
||||
assert.Equal(float64(30.6), scratch.Get("float1"))
|
||||
|
||||
scratch.Add("string1", "Hello ")
|
||||
scratch.Add("string1", "big ")
|
||||
scratch.Add("string1", "World!")
|
||||
|
||||
assert.Equal(t, "Hello big World!", scratch.Get("string1"))
|
||||
assert.Equal("Hello big World!", scratch.Get("string1"))
|
||||
|
||||
scratch.Add("scratch", scratch)
|
||||
_, err := scratch.Add("scratch", scratch)
|
||||
@@ -53,12 +55,14 @@ func TestScratchAdd(t *testing.T) {
|
||||
|
||||
func TestScratchAddSlice(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert := require.New(t)
|
||||
|
||||
scratch := NewScratch()
|
||||
|
||||
_, err := scratch.Add("intSlice", []int{1, 2})
|
||||
assert.Nil(t, err)
|
||||
assert.NoError(err)
|
||||
_, err = scratch.Add("intSlice", 3)
|
||||
assert.Nil(t, err)
|
||||
assert.NoError(err)
|
||||
|
||||
sl := scratch.Get("intSlice")
|
||||
expected := []int{1, 2, 3}
|
||||
@@ -66,10 +70,9 @@ func TestScratchAddSlice(t *testing.T) {
|
||||
if !reflect.DeepEqual(expected, sl) {
|
||||
t.Errorf("Slice difference, go %q expected %q", sl, expected)
|
||||
}
|
||||
|
||||
_, err = scratch.Add("intSlice", []int{4, 5})
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.NoError(err)
|
||||
|
||||
sl = scratch.Get("intSlice")
|
||||
expected = []int{1, 2, 3, 4, 5}
|
||||
@@ -77,29 +80,61 @@ func TestScratchAddSlice(t *testing.T) {
|
||||
if !reflect.DeepEqual(expected, sl) {
|
||||
t.Errorf("Slice difference, go %q expected %q", sl, expected)
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/gohugoio/hugo/issues/5275
|
||||
func TestScratchAddTypedSliceToInterfaceSlice(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert := require.New(t)
|
||||
|
||||
scratch := NewScratch()
|
||||
scratch.Set("slice", []interface{}{})
|
||||
|
||||
_, err := scratch.Add("slice", []int{1, 2})
|
||||
assert.NoError(err)
|
||||
assert.Equal([]int{1, 2}, scratch.Get("slice"))
|
||||
|
||||
}
|
||||
|
||||
// https://github.com/gohugoio/hugo/issues/5361
|
||||
func TestScratchAddDifferentTypedSliceToInterfaceSlice(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert := require.New(t)
|
||||
|
||||
scratch := NewScratch()
|
||||
scratch.Set("slice", []string{"foo"})
|
||||
|
||||
_, err := scratch.Add("slice", []int{1, 2})
|
||||
assert.NoError(err)
|
||||
assert.Equal([]interface{}{"foo", 1, 2}, scratch.Get("slice"))
|
||||
|
||||
}
|
||||
|
||||
func TestScratchSet(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert := require.New(t)
|
||||
|
||||
scratch := NewScratch()
|
||||
scratch.Set("key", "val")
|
||||
assert.Equal(t, "val", scratch.Get("key"))
|
||||
assert.Equal("val", scratch.Get("key"))
|
||||
}
|
||||
|
||||
func TestScratchDelete(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert := require.New(t)
|
||||
|
||||
scratch := NewScratch()
|
||||
scratch.Set("key", "val")
|
||||
scratch.Delete("key")
|
||||
scratch.Add("key", "Lucy Parsons")
|
||||
assert.Equal(t, "Lucy Parsons", scratch.Get("key"))
|
||||
assert.Equal("Lucy Parsons", scratch.Get("key"))
|
||||
}
|
||||
|
||||
// Issue #2005
|
||||
func TestScratchInParallel(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
scratch := NewScratch()
|
||||
|
||||
key := "counter"
|
||||
scratch.Set(key, int64(1))
|
||||
for i := 1; i <= 10; i++ {
|
||||
@@ -142,13 +177,15 @@ func TestScratchGet(t *testing.T) {
|
||||
|
||||
func TestScratchSetInMap(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert := require.New(t)
|
||||
|
||||
scratch := NewScratch()
|
||||
scratch.SetInMap("key", "lux", "Lux")
|
||||
scratch.SetInMap("key", "abc", "Abc")
|
||||
scratch.SetInMap("key", "zyx", "Zyx")
|
||||
scratch.SetInMap("key", "abc", "Abc (updated)")
|
||||
scratch.SetInMap("key", "def", "Def")
|
||||
assert.Equal(t, []interface{}{0: "Abc (updated)", 1: "Def", 2: "Lux", 3: "Zyx"}, scratch.GetSortedMapValues("key"))
|
||||
assert.Equal([]interface{}{0: "Abc (updated)", 1: "Def", 2: "Lux", 3: "Zyx"}, scratch.GetSortedMapValues("key"))
|
||||
}
|
||||
|
||||
func TestScratchGetSortedMapValues(t *testing.T) {
|
||||
|
||||
+7
-7
@@ -46,7 +46,7 @@ func DoArithmetic(a, b interface{}, op rune) (interface{}, error) {
|
||||
bu = 0
|
||||
}
|
||||
default:
|
||||
return nil, errors.New("Can't apply the operator to the values")
|
||||
return nil, errors.New("can't apply the operator to the values")
|
||||
}
|
||||
case reflect.Float32, reflect.Float64:
|
||||
af = av.Float()
|
||||
@@ -58,7 +58,7 @@ func DoArithmetic(a, b interface{}, op rune) (interface{}, error) {
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
bf = float64(bv.Uint()) // may overflow
|
||||
default:
|
||||
return nil, errors.New("Can't apply the operator to the values")
|
||||
return nil, errors.New("can't apply the operator to the values")
|
||||
}
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
au = av.Uint()
|
||||
@@ -79,7 +79,7 @@ func DoArithmetic(a, b interface{}, op rune) (interface{}, error) {
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
bu = bv.Uint()
|
||||
default:
|
||||
return nil, errors.New("Can't apply the operator to the values")
|
||||
return nil, errors.New("can't apply the operator to the values")
|
||||
}
|
||||
case reflect.String:
|
||||
as := av.String()
|
||||
@@ -87,9 +87,9 @@ func DoArithmetic(a, b interface{}, op rune) (interface{}, error) {
|
||||
bs := bv.String()
|
||||
return as + bs, nil
|
||||
}
|
||||
return nil, errors.New("Can't apply the operator to the values")
|
||||
return nil, errors.New("can't apply the operator to the values")
|
||||
default:
|
||||
return nil, errors.New("Can't apply the operator to the values")
|
||||
return nil, errors.New("can't apply the operator to the values")
|
||||
}
|
||||
|
||||
switch op {
|
||||
@@ -128,8 +128,8 @@ func DoArithmetic(a, b interface{}, op rune) (interface{}, error) {
|
||||
} else if bu != 0 {
|
||||
return au / bu, nil
|
||||
}
|
||||
return nil, errors.New("Can't divide the value by 0")
|
||||
return nil, errors.New("can't divide the value by 0")
|
||||
default:
|
||||
return nil, errors.New("There is no such an operation")
|
||||
return nil, errors.New("there is no such an operation")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright 2018 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 terminal contains helper for the terminal, such as coloring output.
|
||||
package terminal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
isatty "github.com/mattn/go-isatty"
|
||||
)
|
||||
|
||||
const (
|
||||
errorColor = "\033[1;31m%s\033[0m"
|
||||
warningColor = "\033[0;33m%s\033[0m"
|
||||
noticeColor = "\033[1;36m%s\033[0m"
|
||||
)
|
||||
|
||||
// IsTerminal return true if the file descriptor is terminal and the TERM
|
||||
// environment variable isn't a dumb one.
|
||||
func IsTerminal(f *os.File) bool {
|
||||
if runtime.GOOS == "windows" {
|
||||
return false
|
||||
}
|
||||
|
||||
fd := f.Fd()
|
||||
return os.Getenv("TERM") != "dumb" && (isatty.IsTerminal(fd) || isatty.IsCygwinTerminal(fd))
|
||||
}
|
||||
|
||||
// Notice colorizes the string in a noticeable color.
|
||||
func Notice(s string) string {
|
||||
return colorize(s, noticeColor)
|
||||
}
|
||||
|
||||
// Error colorizes the string in a colour that grabs attention.
|
||||
func Error(s string) string {
|
||||
return colorize(s, errorColor)
|
||||
}
|
||||
|
||||
// Warning colorizes the string in a colour that warns.
|
||||
func Warning(s string) string {
|
||||
return colorize(s, warningColor)
|
||||
}
|
||||
|
||||
// colorize s in color.
|
||||
func colorize(s, color string) string {
|
||||
s = fmt.Sprintf(color, doublePercent(s))
|
||||
return singlePercent(s)
|
||||
}
|
||||
|
||||
func doublePercent(str string) string {
|
||||
return strings.Replace(str, "%", "%%", -1)
|
||||
}
|
||||
|
||||
func singlePercent(str string) string {
|
||||
return strings.Replace(str, "%%", "%", -1)
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// Copyright 2018 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 text
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/common/terminal"
|
||||
)
|
||||
|
||||
// Positioner represents a thing that knows its position in a text file or stream,
|
||||
// typically an error.
|
||||
type Positioner interface {
|
||||
Position() Position
|
||||
}
|
||||
|
||||
// Position holds a source position in a text file or stream.
|
||||
type Position struct {
|
||||
Filename string // filename, if any
|
||||
Offset int // byte offset, starting at 0. It's set to -1 if not provided.
|
||||
LineNumber int // line number, starting at 1
|
||||
ColumnNumber int // column number, starting at 1 (character count per line)
|
||||
}
|
||||
|
||||
func (pos Position) String() string {
|
||||
if pos.Filename == "" {
|
||||
pos.Filename = "<stream>"
|
||||
}
|
||||
return positionStringFormatfunc(pos)
|
||||
}
|
||||
|
||||
// IsValid returns true if line number is > 0.
|
||||
func (pos Position) IsValid() bool {
|
||||
return pos.LineNumber > 0
|
||||
}
|
||||
|
||||
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"}
|
||||
var identifiersFound []string
|
||||
|
||||
for i := range formatStr {
|
||||
for _, id := range identifiers {
|
||||
if strings.HasPrefix(formatStr[i:], id) {
|
||||
identifiersFound = append(identifiersFound, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
replacer := strings.NewReplacer(":file", "%s", ":line", "%d", ":col", "%d")
|
||||
format := replacer.Replace(formatStr)
|
||||
|
||||
f := func(pos Position) string {
|
||||
args := make([]interface{}, len(identifiersFound))
|
||||
for i, id := range identifiersFound {
|
||||
switch id {
|
||||
case ":file":
|
||||
args[i] = pos.Filename
|
||||
case ":line":
|
||||
args[i] = pos.LineNumber
|
||||
case ":col":
|
||||
args[i] = pos.ColumnNumber
|
||||
}
|
||||
}
|
||||
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
|
||||
if terminal.IsTerminal(os.Stdout) {
|
||||
return terminal.Notice(msg)
|
||||
}
|
||||
|
||||
return msg
|
||||
}
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
func init() {
|
||||
positionStringFormatfunc = createPositionStringFormatter(os.Getenv("HUGO_FILE_LOG_FORMAT"))
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright 2018 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 text
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPositionStringFormatter(t *testing.T) {
|
||||
assert := require.New(t)
|
||||
|
||||
pos := Position{Filename: "/my/file.txt", LineNumber: 12, ColumnNumber: 13, Offset: 14}
|
||||
|
||||
assert.Equal("/my/file.txt|13|12", createPositionStringFormatter(":file|:col|:line")(pos))
|
||||
assert.Equal("13|/my/file.txt|12", createPositionStringFormatter(":col|:file|:line")(pos))
|
||||
assert.Equal("好:13", createPositionStringFormatter("好::col")(pos))
|
||||
assert.Equal("\"/my/file.txt:12:13\"", createPositionStringFormatter("")(pos))
|
||||
assert.Equal("\"/my/file.txt:12:13\"", pos.String())
|
||||
|
||||
}
|
||||
@@ -52,6 +52,13 @@ func (q *EvictingStringQueue) Add(v string) {
|
||||
q.mu.Unlock()
|
||||
}
|
||||
|
||||
// Contains returns whether the queue contains v.
|
||||
func (q *EvictingStringQueue) Contains(v string) bool {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
return q.set[v]
|
||||
}
|
||||
|
||||
// Peek looks at the last element added to the queue.
|
||||
func (q *EvictingStringQueue) Peek() string {
|
||||
q.mu.Lock()
|
||||
|
||||
@@ -36,6 +36,9 @@ func TestEvictingStringQueue(t *testing.T) {
|
||||
queue.Add("a")
|
||||
queue.Add("b")
|
||||
|
||||
assert.True(queue.Contains("a"))
|
||||
assert.False(queue.Contains("foo"))
|
||||
|
||||
assert.Equal([]string{"b", "a"}, queue.PeekAll())
|
||||
assert.Equal("b", queue.Peek())
|
||||
queue.Add("c")
|
||||
|
||||
+31
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright 2017-present The Hugo Authors. All rights reserved.
|
||||
// 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.
|
||||
@@ -16,6 +16,7 @@ package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
@@ -41,6 +42,8 @@ func (k KeyValues) String() string {
|
||||
return fmt.Sprintf("%v: %v", k.Key, k.Values)
|
||||
}
|
||||
|
||||
// NewKeyValuesStrings takes a given key and slice of values and returns a new
|
||||
// KeyValues struct.
|
||||
func NewKeyValuesStrings(key string, values ...string) KeyValues {
|
||||
iv := make([]interface{}, len(values))
|
||||
for i := 0; i < len(values); i++ {
|
||||
@@ -48,3 +51,30 @@ func NewKeyValuesStrings(key string, values ...string) KeyValues {
|
||||
}
|
||||
return KeyValues{Key: key, Values: iv}
|
||||
}
|
||||
|
||||
// Zeroer, as implemented by time.Time, will be used by the truth template
|
||||
// funcs in Hugo (if, with, not, and, or).
|
||||
type Zeroer interface {
|
||||
IsZero() bool
|
||||
}
|
||||
|
||||
// IsNil reports whether v is nil.
|
||||
func IsNil(v interface{}) bool {
|
||||
if v == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
value := reflect.ValueOf(v)
|
||||
switch value.Kind() {
|
||||
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
|
||||
return value.IsNil()
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// DevMarker is a marker interface for types that should only be used during
|
||||
// development.
|
||||
type DevMarker interface {
|
||||
DevOnly()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright 2018 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 urls
|
||||
|
||||
// RefLinker is implemented by those who support reference linking.
|
||||
// args must contain a path, but can also point to the target
|
||||
// language or output format.
|
||||
type RefLinker interface {
|
||||
Ref(args map[string]interface{}) (string, error)
|
||||
RelRef(args map[string]interface{}) (string, error)
|
||||
}
|
||||
@@ -20,6 +20,12 @@ type Eqer interface {
|
||||
Eq(other interface{}) bool
|
||||
}
|
||||
|
||||
// ProbablyEq is an equal check that may return false positives, but never
|
||||
// a false negative.
|
||||
type ProbablyEqer interface {
|
||||
ProbablyEq(other interface{}) bool
|
||||
}
|
||||
|
||||
// Comparer can be used to compare two values.
|
||||
// This will be used when using the le, ge etc. operators in the templates.
|
||||
// Compare returns -1 if the given version is less than, 0 if equal and 1 if greater than
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
// Copyright 2018 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 (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/parser/metadecoders"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var (
|
||||
ValidConfigFileExtensions = []string{"toml", "yaml", "yml", "json"}
|
||||
validConfigFileExtensionsMap map[string]bool = make(map[string]bool)
|
||||
)
|
||||
|
||||
func init() {
|
||||
for _, ext := range ValidConfigFileExtensions {
|
||||
validConfigFileExtensionsMap[ext] = true
|
||||
}
|
||||
}
|
||||
|
||||
// IsValidConfigFilename returns whether filename is one of the supported
|
||||
// config formats in Hugo.
|
||||
func IsValidConfigFilename(filename string) bool {
|
||||
ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(filename), "."))
|
||||
return validConfigFileExtensionsMap[ext]
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// FromFile loads the configuration from the given filename.
|
||||
func FromFile(fs afero.Fs, filename string) (Provider, error) {
|
||||
m, err := loadConfigFromFile(fs, filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v := newViper()
|
||||
|
||||
err = v.MergeConfigMap(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// FromFileToMap is the same as FromFile, but it returns the config values
|
||||
// as a simple map.
|
||||
func FromFileToMap(fs afero.Fs, filename string) (map[string]interface{}, error) {
|
||||
return loadConfigFromFile(fs, filename)
|
||||
}
|
||||
|
||||
func readConfig(format metadecoders.Format, data []byte) (map[string]interface{}, error) {
|
||||
m, err := metadecoders.Default.UnmarshalToMap(data, format)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
RenameKeys(m)
|
||||
|
||||
return m, nil
|
||||
|
||||
}
|
||||
|
||||
func loadConfigFromFile(fs afero.Fs, filename string) (map[string]interface{}, error) {
|
||||
m, err := metadecoders.Default.UnmarshalFileToMap(fs, filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
RenameKeys(m)
|
||||
return m, nil
|
||||
}
|
||||
|
||||
var keyAliases maps.KeyRenamer
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
keyAliases, err = maps.NewKeyRenamer(
|
||||
// Before 0.53 we used singular for "menu".
|
||||
"{menu,languages/*/menu}", "menus",
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// RenameKeys renames config keys in m recursively according to a global Hugo
|
||||
// alias definition.
|
||||
func RenameKeys(m map[string]interface{}) {
|
||||
keyAliases.Rename(m)
|
||||
}
|
||||
|
||||
func newViper() *viper.Viper {
|
||||
v := viper.New()
|
||||
|
||||
return v
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// 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.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestIsValidConfigFileName(t *testing.T) {
|
||||
assert := require.New(t)
|
||||
|
||||
for _, ext := range ValidConfigFileExtensions {
|
||||
filename := "config." + ext
|
||||
assert.True(IsValidConfigFilename(filename), ext)
|
||||
assert.True(IsValidConfigFilename(strings.ToUpper(filename)))
|
||||
}
|
||||
|
||||
assert.False(IsValidConfigFilename(""))
|
||||
assert.False(IsValidConfigFilename("config.toml.swp"))
|
||||
}
|
||||
+21
-20
@@ -1,4 +1,4 @@
|
||||
// Copyright 2017-present The Hugo Authors. All rights reserved.
|
||||
// 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.
|
||||
@@ -14,11 +14,7 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Provider provides the configuration settings for Hugo.
|
||||
@@ -34,24 +30,29 @@ type Provider interface {
|
||||
IsSet(key 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 := viper.New()
|
||||
v.SetConfigType(configType)
|
||||
if err := v.ReadConfig(strings.NewReader(config)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// GetStringSlicePreserveString returns a string slice from the given config and key.
|
||||
// It differs from the GetStringSlice method in that if the config value is a string,
|
||||
// we do not attempt to split it into fields.
|
||||
func GetStringSlicePreserveString(cfg Provider, key string) []string {
|
||||
sd := cfg.Get(key)
|
||||
if sds, ok := sd.(string); ok {
|
||||
return []string{sds}
|
||||
} else {
|
||||
return cast.ToStringSlice(sd)
|
||||
}
|
||||
return toStringSlicePreserveString(sd)
|
||||
}
|
||||
|
||||
func toStringSlicePreserveString(v interface{}) []string {
|
||||
if sds, ok := v.(string); ok {
|
||||
return []string{sds}
|
||||
}
|
||||
return cast.ToStringSlice(v)
|
||||
}
|
||||
|
||||
// SetBaseTestDefaults provides some common config defaults used in tests.
|
||||
func SetBaseTestDefaults(cfg Provider) {
|
||||
cfg.Set("resourceDir", "resources")
|
||||
cfg.Set("contentDir", "content")
|
||||
cfg.Set("dataDir", "data")
|
||||
cfg.Set("i18nDir", "i18n")
|
||||
cfg.Set("layoutDir", "layouts")
|
||||
cfg.Set("assetDir", "assets")
|
||||
cfg.Set("archetypeDir", "archetypes")
|
||||
cfg.Set("publishDir", "public")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
// 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.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GetNumWorkerMultiplier returns the base value used to calculate the number
|
||||
// of workers to use for Hugo's parallel execution.
|
||||
// It returns the value in HUGO_NUMWORKERMULTIPLIER OS env variable if set to a
|
||||
// positive integer, else the number of logical CPUs.
|
||||
func GetNumWorkerMultiplier() int {
|
||||
if gmp := os.Getenv("HUGO_NUMWORKERMULTIPLIER"); gmp != "" {
|
||||
if p, err := strconv.Atoi(gmp); err == nil && p > 0 {
|
||||
return p
|
||||
}
|
||||
}
|
||||
return runtime.NumCPU()
|
||||
}
|
||||
|
||||
// SetEnvVars sets vars on the form key=value in the oldVars slice.
|
||||
func SetEnvVars(oldVars *[]string, keyValues ...string) {
|
||||
for i := 0; i < len(keyValues); i += 2 {
|
||||
setEnvVar(oldVars, keyValues[i], keyValues[i+1])
|
||||
}
|
||||
}
|
||||
|
||||
func SplitEnvVar(v string) (string, string) {
|
||||
parts := strings.Split(v, "=")
|
||||
return parts[0], parts[1]
|
||||
}
|
||||
|
||||
func setEnvVar(vars *[]string, key, value string) {
|
||||
for i := range *vars {
|
||||
if strings.HasPrefix((*vars)[i], key+"=") {
|
||||
(*vars)[i] = key + "=" + value
|
||||
return
|
||||
}
|
||||
}
|
||||
// New var.
|
||||
*vars = append(*vars, key+"="+value)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// 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.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSetEnvVars(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert := require.New(t)
|
||||
vars := []string{"FOO=bar", "HUGO=cool", "BAR=foo"}
|
||||
SetEnvVars(&vars, "HUGO", "rocking!", "NEW", "bar")
|
||||
assert.Equal([]string{"FOO=bar", "HUGO=rocking!", "BAR=foo", "NEW=bar"}, vars)
|
||||
|
||||
key, val := SplitEnvVar("HUGO=rocks")
|
||||
assert.Equal("HUGO", key)
|
||||
assert.Equal("rocks", val)
|
||||
}
|
||||
@@ -96,6 +96,7 @@ type YouTube struct {
|
||||
PrivacyEnhanced bool
|
||||
}
|
||||
|
||||
// DecodeConfig creates a privacy Config from a given Hugo configuration.
|
||||
func DecodeConfig(cfg config.Provider) (pc Config, err error) {
|
||||
if !cfg.IsSet(privacyConfigKey) {
|
||||
return
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
// 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.
|
||||
@@ -23,6 +23,7 @@ const (
|
||||
|
||||
disqusShortnameKey = "disqusshortname"
|
||||
googleAnalyticsKey = "googleanalytics"
|
||||
rssLimitKey = "rssLimit"
|
||||
)
|
||||
|
||||
// Config is a privacy configuration for all the relevant services in Hugo.
|
||||
@@ -31,6 +32,7 @@ type Config struct {
|
||||
GoogleAnalytics GoogleAnalytics
|
||||
Instagram Instagram
|
||||
Twitter Twitter
|
||||
RSS RSS
|
||||
}
|
||||
|
||||
// Disqus holds the functional configuration settings related to the Disqus template.
|
||||
@@ -61,6 +63,13 @@ type Twitter struct {
|
||||
DisableInlineCSS bool
|
||||
}
|
||||
|
||||
// RSS holds the functional configuration settings related to the RSS feeds.
|
||||
type RSS struct {
|
||||
// Limit the number of pages.
|
||||
Limit int
|
||||
}
|
||||
|
||||
// DecodeConfig creates a services Config from a given Hugo configuration.
|
||||
func DecodeConfig(cfg config.Provider) (c Config, err error) {
|
||||
m := cfg.GetStringMap(servicesConfigKey)
|
||||
|
||||
@@ -75,5 +84,9 @@ func DecodeConfig(cfg config.Provider) (c Config, err error) {
|
||||
c.Disqus.Shortname = cfg.GetString(disqusShortnameKey)
|
||||
}
|
||||
|
||||
if c.RSS.Limit == 0 {
|
||||
c.RSS.Limit = cfg.GetInt(rssLimitKey)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2015 The Hugo Authors. All rights reserved.
|
||||
// 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.
|
||||
@@ -11,7 +11,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package hugolib
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/spf13/cast"
|
||||
@@ -25,21 +25,20 @@ type Sitemap struct {
|
||||
Filename string
|
||||
}
|
||||
|
||||
func parseSitemap(input map[string]interface{}) Sitemap {
|
||||
sitemap := Sitemap{Priority: -1, Filename: "sitemap.xml"}
|
||||
func DecodeSitemap(prototype Sitemap, input map[string]interface{}) Sitemap {
|
||||
|
||||
for key, value := range input {
|
||||
switch key {
|
||||
case "changefreq":
|
||||
sitemap.ChangeFreq = cast.ToString(value)
|
||||
prototype.ChangeFreq = cast.ToString(value)
|
||||
case "priority":
|
||||
sitemap.Priority = cast.ToFloat64(value)
|
||||
prototype.Priority = cast.ToFloat64(value)
|
||||
case "filename":
|
||||
sitemap.Filename = cast.ToString(value)
|
||||
prototype.Filename = cast.ToString(value)
|
||||
default:
|
||||
jww.WARN.Printf("Unknown Sitemap field: %s\n", key)
|
||||
}
|
||||
}
|
||||
|
||||
return sitemap
|
||||
return prototype
|
||||
}
|
||||
+270
-52
@@ -1,4 +1,4 @@
|
||||
// Copyright 2016 The Hugo Authors. All rights reserved.
|
||||
// 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.
|
||||
@@ -16,72 +16,85 @@ package create
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/hugofs/files"
|
||||
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
"github.com/spf13/afero"
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
)
|
||||
|
||||
// NewContent creates a new content file in the content directory based upon the
|
||||
// given kind, which is used to lookup an archetype.
|
||||
func NewContent(
|
||||
ps *helpers.PathSpec,
|
||||
siteFactory func(filename string, siteUsed bool) (*hugolib.Site, error), kind, targetPath string) error {
|
||||
sites *hugolib.HugoSites, kind, targetPath string) error {
|
||||
targetPath = filepath.Clean(targetPath)
|
||||
ext := helpers.Ext(targetPath)
|
||||
fs := ps.BaseFs.SourceFilesystems.Archetypes.Fs
|
||||
ps := sites.PathSpec
|
||||
archetypeFs := ps.BaseFs.SourceFilesystems.Archetypes.Fs
|
||||
sourceFs := ps.Fs.Source
|
||||
|
||||
jww.INFO.Printf("attempting to create %q of %q of ext %q", targetPath, kind, ext)
|
||||
|
||||
archetypeFilename := findArchetype(ps, kind, ext)
|
||||
archetypeFilename, isDir := findArchetype(ps, kind, ext)
|
||||
contentPath, s := resolveContentPath(sites, sourceFs, targetPath)
|
||||
|
||||
if isDir {
|
||||
|
||||
langFs, err := hugofs.NewLanguageFs(sites.LanguageSet(), archetypeFs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cm, err := mapArcheTypeDir(ps, langFs, archetypeFilename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cm.siteUsed {
|
||||
if err := sites.Build(hugolib.BuildCfg{SkipRender: true}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
name := filepath.Base(targetPath)
|
||||
return newContentFromDir(archetypeFilename, sites, sourceFs, cm, name, contentPath)
|
||||
}
|
||||
|
||||
// Building the sites can be expensive, so only do it if really needed.
|
||||
siteUsed := false
|
||||
|
||||
if archetypeFilename != "" {
|
||||
f, err := fs.Open(archetypeFilename)
|
||||
|
||||
var err error
|
||||
siteUsed, err = usesSiteVar(archetypeFs, archetypeFilename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open archetype file: %s", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if helpers.ReaderContains(f, []byte(".Site")) {
|
||||
siteUsed = true
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
s, err := siteFactory(targetPath, siteUsed)
|
||||
if siteUsed {
|
||||
if err := sites.Build(hugolib.BuildCfg{SkipRender: true}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
content, err := executeArcheTypeAsTemplate(s, "", kind, targetPath, archetypeFilename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var content []byte
|
||||
|
||||
content, err = executeArcheTypeAsTemplate(s, kind, targetPath, archetypeFilename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The site may have multiple content dirs, and we currently do not know which contentDir the
|
||||
// user wants to create this content in. We should improve on this, but we start by testing if the
|
||||
// provided path points to an existing dir. If so, use it as is.
|
||||
var contentPath string
|
||||
var exists bool
|
||||
targetDir := filepath.Dir(targetPath)
|
||||
|
||||
if targetDir != "" && targetDir != "." {
|
||||
exists, _ = helpers.Exists(targetDir, fs)
|
||||
}
|
||||
|
||||
if exists {
|
||||
contentPath = targetPath
|
||||
} else {
|
||||
contentPath = s.PathSpec.AbsPathify(filepath.Join(s.Cfg.GetString("contentDir"), targetPath))
|
||||
}
|
||||
|
||||
if err := helpers.SafeWriteToDisk(contentPath, bytes.NewReader(content), s.Fs.Source); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -103,29 +116,234 @@ func NewContent(
|
||||
return nil
|
||||
}
|
||||
|
||||
func targetSite(sites *hugolib.HugoSites, fi hugofs.FileMetaInfo) *hugolib.Site {
|
||||
for _, s := range sites.Sites {
|
||||
if fi.Meta().Lang() == s.Language().Lang {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return sites.Sites[0]
|
||||
}
|
||||
|
||||
func newContentFromDir(
|
||||
archetypeDir string,
|
||||
sites *hugolib.HugoSites,
|
||||
targetFs afero.Fs,
|
||||
cm archetypeMap, name, targetPath string) error {
|
||||
|
||||
for _, f := range cm.otherFiles {
|
||||
meta := f.Meta()
|
||||
filename := meta.Path()
|
||||
// Just copy the file to destination.
|
||||
in, err := meta.Open()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to open non-content file")
|
||||
}
|
||||
|
||||
targetFilename := filepath.Join(targetPath, strings.TrimPrefix(filename, archetypeDir))
|
||||
|
||||
targetDir := filepath.Dir(targetFilename)
|
||||
if err := targetFs.MkdirAll(targetDir, 0777); err != nil && !os.IsExist(err) {
|
||||
return errors.Wrapf(err, "failed to create target directory for %s:", targetDir)
|
||||
}
|
||||
|
||||
out, err := targetFs.Create(targetFilename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = io.Copy(out, in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
in.Close()
|
||||
out.Close()
|
||||
}
|
||||
|
||||
for _, f := range cm.contentFiles {
|
||||
filename := f.Meta().Path()
|
||||
s := targetSite(sites, f)
|
||||
targetFilename := filepath.Join(targetPath, strings.TrimPrefix(filename, archetypeDir))
|
||||
|
||||
content, err := executeArcheTypeAsTemplate(s, name, archetypeDir, targetFilename, filename)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to execute archetype template")
|
||||
}
|
||||
|
||||
if err := helpers.SafeWriteToDisk(targetFilename, bytes.NewReader(content), targetFs); err != nil {
|
||||
return errors.Wrap(err, "failed to save results")
|
||||
}
|
||||
}
|
||||
|
||||
jww.FEEDBACK.Println(targetPath, "created")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type archetypeMap struct {
|
||||
// These needs to be parsed and executed as Go templates.
|
||||
contentFiles []hugofs.FileMetaInfo
|
||||
// These are just copied to destination.
|
||||
otherFiles []hugofs.FileMetaInfo
|
||||
// If the templates needs a fully built site. This can potentially be
|
||||
// expensive, so only do when needed.
|
||||
siteUsed bool
|
||||
}
|
||||
|
||||
func mapArcheTypeDir(
|
||||
ps *helpers.PathSpec,
|
||||
fs afero.Fs,
|
||||
archetypeDir string) (archetypeMap, error) {
|
||||
|
||||
var m archetypeMap
|
||||
|
||||
walkFn := func(path string, fi hugofs.FileMetaInfo, err error) error {
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if fi.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
fil := fi.(hugofs.FileMetaInfo)
|
||||
|
||||
if files.IsContentFile(path) {
|
||||
m.contentFiles = append(m.contentFiles, fil)
|
||||
if !m.siteUsed {
|
||||
m.siteUsed, err = usesSiteVar(fs, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
m.otherFiles = append(m.otherFiles, fil)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
walkCfg := hugofs.WalkwayConfig{
|
||||
WalkFn: walkFn,
|
||||
Fs: fs,
|
||||
Root: archetypeDir,
|
||||
}
|
||||
|
||||
w := hugofs.NewWalkway(walkCfg)
|
||||
|
||||
if err := w.Walk(); err != nil {
|
||||
return m, errors.Wrapf(err, "failed to walk archetype dir %q", archetypeDir)
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func usesSiteVar(fs afero.Fs, filename string) (bool, error) {
|
||||
f, err := fs.Open(filename)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "failed to open archetype file")
|
||||
}
|
||||
defer f.Close()
|
||||
return helpers.ReaderContains(f, []byte(".Site")), nil
|
||||
}
|
||||
|
||||
// Resolve the target content path.
|
||||
func resolveContentPath(sites *hugolib.HugoSites, fs afero.Fs, targetPath string) (string, *hugolib.Site) {
|
||||
targetDir := filepath.Dir(targetPath)
|
||||
first := sites.Sites[0]
|
||||
|
||||
var (
|
||||
s *hugolib.Site
|
||||
siteContentDir string
|
||||
)
|
||||
|
||||
// Try the filename: my-post.en.md
|
||||
for _, ss := range sites.Sites {
|
||||
if strings.Contains(targetPath, "."+ss.Language().Lang+".") {
|
||||
s = ss
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var dirLang string
|
||||
|
||||
for _, dir := range sites.BaseFs.Content.Dirs {
|
||||
meta := dir.Meta()
|
||||
contentDir := meta.Filename()
|
||||
|
||||
if !strings.HasSuffix(contentDir, helpers.FilePathSeparator) {
|
||||
contentDir += helpers.FilePathSeparator
|
||||
}
|
||||
|
||||
if strings.HasPrefix(targetPath, contentDir) {
|
||||
siteContentDir = contentDir
|
||||
dirLang = meta.Lang()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if s == nil && dirLang != "" {
|
||||
for _, ss := range sites.Sites {
|
||||
if ss.Lang() == dirLang {
|
||||
s = ss
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if s == nil {
|
||||
s = first
|
||||
}
|
||||
|
||||
if targetDir != "" && targetDir != "." {
|
||||
exists, _ := helpers.Exists(targetDir, fs)
|
||||
|
||||
if exists {
|
||||
return targetPath, s
|
||||
}
|
||||
}
|
||||
|
||||
if siteContentDir == "" {
|
||||
|
||||
}
|
||||
|
||||
if siteContentDir != "" {
|
||||
pp := filepath.Join(siteContentDir, strings.TrimPrefix(targetPath, siteContentDir))
|
||||
return s.PathSpec.AbsPathify(pp), s
|
||||
} else {
|
||||
var contentDir string
|
||||
for _, dir := range sites.BaseFs.Content.Dirs {
|
||||
contentDir = dir.Meta().Filename()
|
||||
if dir.Meta().Lang() == s.Lang() {
|
||||
break
|
||||
}
|
||||
}
|
||||
return s.PathSpec.AbsPathify(filepath.Join(contentDir, targetPath)), s
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// FindArchetype takes a given kind/archetype of content and returns the path
|
||||
// to the archetype in the archetype filesystem, blank if none found.
|
||||
func findArchetype(ps *helpers.PathSpec, kind, ext string) (outpath string) {
|
||||
func findArchetype(ps *helpers.PathSpec, kind, ext string) (outpath string, isDir bool) {
|
||||
fs := ps.BaseFs.Archetypes.Fs
|
||||
|
||||
// If the new content isn't in a subdirectory, kind == "".
|
||||
// Therefore it should be excluded otherwise `is a directory`
|
||||
// error will occur. github.com/gohugoio/hugo/issues/411
|
||||
var pathsToCheck = []string{"default"}
|
||||
var pathsToCheck []string
|
||||
|
||||
if ext != "" {
|
||||
if kind != "" {
|
||||
pathsToCheck = append([]string{kind + ext, "default" + ext}, pathsToCheck...)
|
||||
} else {
|
||||
pathsToCheck = append([]string{"default" + ext}, pathsToCheck...)
|
||||
}
|
||||
if kind != "" {
|
||||
pathsToCheck = append(pathsToCheck, kind+ext)
|
||||
}
|
||||
pathsToCheck = append(pathsToCheck, "default"+ext, "default")
|
||||
|
||||
for _, p := range pathsToCheck {
|
||||
if exists, _ := helpers.Exists(p, fs); exists {
|
||||
return p
|
||||
fi, err := fs.Stat(p)
|
||||
if err == nil {
|
||||
return p, fi.IsDir()
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
return "", false
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/source"
|
||||
|
||||
@@ -80,7 +82,7 @@ var (
|
||||
"%}x}", "%}}")
|
||||
)
|
||||
|
||||
func executeArcheTypeAsTemplate(s *hugolib.Site, kind, targetPath, archetypeFilename string) ([]byte, error) {
|
||||
func executeArcheTypeAsTemplate(s *hugolib.Site, name, kind, targetPath, archetypeFilename string) ([]byte, error) {
|
||||
|
||||
var (
|
||||
archetypeContent []byte
|
||||
@@ -88,20 +90,19 @@ func executeArcheTypeAsTemplate(s *hugolib.Site, kind, targetPath, archetypeFile
|
||||
err error
|
||||
)
|
||||
|
||||
ps, err := helpers.NewPathSpec(s.Deps.Fs, s.Deps.Cfg)
|
||||
f, err := s.SourceSpec.NewFileInfoFrom(targetPath, targetPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sp := source.NewSourceSpec(ps, ps.Fs.Source)
|
||||
|
||||
f := sp.NewFileInfo("", targetPath, false, nil)
|
||||
if name == "" {
|
||||
name = f.TranslationBaseName()
|
||||
|
||||
name := f.TranslationBaseName()
|
||||
|
||||
if name == "index" || name == "_index" {
|
||||
// Page bundles; the directory name will hopefully have a better name.
|
||||
dir := strings.TrimSuffix(f.Dir(), helpers.FilePathSeparator)
|
||||
_, name = filepath.Split(dir)
|
||||
if name == "index" || name == "_index" {
|
||||
// Page bundles; the directory name will hopefully have a better name.
|
||||
dir := strings.TrimSuffix(f.Dir(), helpers.FilePathSeparator)
|
||||
_, name = filepath.Split(dir)
|
||||
}
|
||||
}
|
||||
|
||||
data := ArchetypeFileData{
|
||||
@@ -131,14 +132,14 @@ func executeArcheTypeAsTemplate(s *hugolib.Site, kind, targetPath, archetypeFile
|
||||
templateHandler := s.Deps.Tmpl.(tpl.TemplateHandler)
|
||||
templateName := "_text/" + helpers.Filename(archetypeFilename)
|
||||
if err := templateHandler.AddTemplate(templateName, string(archetypeTemplate)); err != nil {
|
||||
return nil, fmt.Errorf("Failed to parse archetype file %q: %s", archetypeFilename, err)
|
||||
return nil, errors.Wrapf(err, "Failed to parse archetype file %q:", archetypeFilename)
|
||||
}
|
||||
|
||||
templ, _ := templateHandler.Lookup(templateName)
|
||||
|
||||
var buff bytes.Buffer
|
||||
if err := templ.Execute(&buff, data); err != nil {
|
||||
return nil, fmt.Errorf("Failed to process archetype file %q: %s", archetypeFilename, err)
|
||||
return nil, errors.Wrapf(err, "Failed to process archetype file %q:", archetypeFilename)
|
||||
}
|
||||
|
||||
archetypeContent = []byte(archetypeShortcodeReplacementsPost.Replace(buff.String()))
|
||||
|
||||
+125
-46
@@ -35,8 +35,6 @@ import (
|
||||
)
|
||||
|
||||
func TestNewContent(t *testing.T) {
|
||||
v := viper.New()
|
||||
initViper(v)
|
||||
|
||||
cases := []struct {
|
||||
kind string
|
||||
@@ -49,6 +47,14 @@ func TestNewContent(t *testing.T) {
|
||||
{"stump", "stump/sample-2.md", []string{`title: "Sample 2"`}}, // no archetype file
|
||||
{"", "sample-3.md", []string{`title: "Sample 3"`}}, // no archetype
|
||||
{"product", "product/sample-4.md", []string{`title = "SAMPLE-4"`}}, // empty archetype front matter
|
||||
{"lang", "post/lang-1.md", []string{`Site Lang: en|Name: Lang 1|i18n: Hugo Rocks!`}},
|
||||
{"lang", "post/lang-2.en.md", []string{`Site Lang: en|Name: Lang 2|i18n: Hugo Rocks!`}},
|
||||
{"lang", "content/post/lang-3.nn.md", []string{`Site Lang: nn|Name: Lang 3|i18n: Hugo Rokkar!`}},
|
||||
{"lang", "content_nn/post/lang-4.md", []string{`Site Lang: nn|Name: Lang 4|i18n: Hugo Rokkar!`}},
|
||||
{"lang", "content_nn/post/lang-5.en.md", []string{`Site Lang: en|Name: Lang 5|i18n: Hugo Rocks!`}},
|
||||
{"lang", "post/my-bundle/index.md", []string{`Site Lang: en|Name: My Bundle|i18n: Hugo Rocks!`}},
|
||||
{"lang", "post/my-bundle/index.en.md", []string{`Site Lang: en|Name: My Bundle|i18n: Hugo Rocks!`}},
|
||||
{"lang", "content/post/my-bundle/index.nn.md", []string{`Site Lang: nn|Name: My Bundle|i18n: Hugo Rokkar!`}},
|
||||
{"shortcodes", "shortcodes/go.md", []string{
|
||||
`title = "GO"`,
|
||||
"{{< myshortcode >}}",
|
||||
@@ -56,43 +62,88 @@ func TestNewContent(t *testing.T) {
|
||||
"{{</* comment */>}}\n{{%/* comment */%}}"}}, // shortcodes
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
cfg, fs := newTestCfg()
|
||||
require.NoError(t, initFs(fs))
|
||||
h, err := hugolib.NewHugoSites(deps.DepsCfg{Cfg: cfg, Fs: fs})
|
||||
require.NoError(t, err)
|
||||
for i, c := range cases {
|
||||
c := c
|
||||
t.Run(fmt.Sprintf("%s-%d", c.kind, i), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert := require.New(t)
|
||||
mm := afero.NewMemMapFs()
|
||||
assert.NoError(initFs(mm))
|
||||
cfg, fs := newTestCfg(assert, mm)
|
||||
h, err := hugolib.NewHugoSites(deps.DepsCfg{Cfg: cfg, Fs: fs})
|
||||
assert.NoError(err)
|
||||
|
||||
siteFactory := func(filename string, siteUsed bool) (*hugolib.Site, error) {
|
||||
return h.Sites[0], nil
|
||||
}
|
||||
assert.NoError(create.NewContent(h, c.kind, c.path))
|
||||
|
||||
require.NoError(t, create.NewContent(h.PathSpec, siteFactory, c.kind, c.path))
|
||||
|
||||
fname := filepath.Join("content", filepath.FromSlash(c.path))
|
||||
content := readFileFromFs(t, fs.Source, fname)
|
||||
for i, v := range c.expected {
|
||||
found := strings.Contains(content, v)
|
||||
if !found {
|
||||
t.Fatalf("[%d] %q missing from output:\n%q", i, v, content)
|
||||
fname := filepath.FromSlash(c.path)
|
||||
if !strings.HasPrefix(fname, "content") {
|
||||
fname = filepath.Join("content", fname)
|
||||
}
|
||||
}
|
||||
content := readFileFromFs(t, fs.Source, fname)
|
||||
for _, v := range c.expected {
|
||||
found := strings.Contains(content, v)
|
||||
if !found {
|
||||
t.Fatalf("[%d] %q missing from output:\n%q", i, v, content)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func initViper(v *viper.Viper) {
|
||||
v.Set("metaDataFormat", "toml")
|
||||
v.Set("archetypeDir", "archetypes")
|
||||
v.Set("contentDir", "content")
|
||||
v.Set("themesDir", "themes")
|
||||
v.Set("layoutDir", "layouts")
|
||||
v.Set("i18nDir", "i18n")
|
||||
v.Set("theme", "sample")
|
||||
v.Set("archetypeDir", "archetypes")
|
||||
v.Set("resourceDir", "resources")
|
||||
v.Set("publishDir", "public")
|
||||
func TestNewContentFromDir(t *testing.T) {
|
||||
mm := afero.NewMemMapFs()
|
||||
assert := require.New(t)
|
||||
|
||||
archetypeDir := filepath.Join("archetypes", "my-bundle")
|
||||
assert.NoError(mm.MkdirAll(archetypeDir, 0755))
|
||||
|
||||
archetypeThemeDir := filepath.Join("themes", "mytheme", "archetypes", "my-theme-bundle")
|
||||
assert.NoError(mm.MkdirAll(archetypeThemeDir, 0755))
|
||||
|
||||
contentFile := `
|
||||
File: %s
|
||||
Site Lang: {{ .Site.Language.Lang }}
|
||||
Name: {{ replace .Name "-" " " | title }}
|
||||
i18n: {{ T "hugo" }}
|
||||
`
|
||||
|
||||
assert.NoError(afero.WriteFile(mm, filepath.Join(archetypeDir, "index.md"), []byte(fmt.Sprintf(contentFile, "index.md")), 0755))
|
||||
assert.NoError(afero.WriteFile(mm, filepath.Join(archetypeDir, "index.nn.md"), []byte(fmt.Sprintf(contentFile, "index.nn.md")), 0755))
|
||||
|
||||
assert.NoError(afero.WriteFile(mm, filepath.Join(archetypeDir, "pages", "bio.md"), []byte(fmt.Sprintf(contentFile, "bio.md")), 0755))
|
||||
assert.NoError(afero.WriteFile(mm, filepath.Join(archetypeDir, "resources", "hugo1.json"), []byte(`hugo1: {{ printf "no template handling in here" }}`), 0755))
|
||||
assert.NoError(afero.WriteFile(mm, filepath.Join(archetypeDir, "resources", "hugo2.xml"), []byte(`hugo2: {{ printf "no template handling in here" }}`), 0755))
|
||||
|
||||
assert.NoError(afero.WriteFile(mm, filepath.Join(archetypeThemeDir, "index.md"), []byte(fmt.Sprintf(contentFile, "index.md")), 0755))
|
||||
assert.NoError(afero.WriteFile(mm, filepath.Join(archetypeThemeDir, "resources", "hugo1.json"), []byte(`hugo1: {{ printf "no template handling in here" }}`), 0755))
|
||||
|
||||
assert.NoError(initFs(mm))
|
||||
cfg, fs := newTestCfg(assert, mm)
|
||||
|
||||
h, err := hugolib.NewHugoSites(deps.DepsCfg{Cfg: cfg, Fs: fs})
|
||||
assert.NoError(err)
|
||||
assert.Equal(2, len(h.Sites))
|
||||
|
||||
assert.NoError(create.NewContent(h, "my-bundle", "post/my-post"))
|
||||
|
||||
assertContains(assert, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/resources/hugo1.json")), `hugo1: {{ printf "no template handling in here" }}`)
|
||||
assertContains(assert, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/resources/hugo2.xml")), `hugo2: {{ printf "no template handling in here" }}`)
|
||||
|
||||
// Content files should get the correct site context.
|
||||
// TODO(bep) archetype check i18n
|
||||
assertContains(assert, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/index.md")), `File: index.md`, `Site Lang: en`, `Name: My Post`, `i18n: Hugo Rocks!`)
|
||||
assertContains(assert, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/index.nn.md")), `File: index.nn.md`, `Site Lang: nn`, `Name: My Post`, `i18n: Hugo Rokkar!`)
|
||||
|
||||
assertContains(assert, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/pages/bio.md")), `File: bio.md`, `Site Lang: en`, `Name: My Post`)
|
||||
|
||||
assert.NoError(create.NewContent(h, "my-theme-bundle", "post/my-theme-post"))
|
||||
assertContains(assert, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-theme-post/index.md")), `File: index.md`, `Site Lang: en`, `Name: My Theme Post`, `i18n: Hugo Rocks!`)
|
||||
assertContains(assert, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-theme-post/resources/hugo1.json")), `hugo1: {{ printf "no template handling in here" }}`)
|
||||
|
||||
}
|
||||
|
||||
func initFs(fs *hugofs.Fs) error {
|
||||
func initFs(fs afero.Fs) error {
|
||||
perm := os.FileMode(0755)
|
||||
var err error
|
||||
|
||||
@@ -103,8 +154,8 @@ func initFs(fs *hugofs.Fs) error {
|
||||
filepath.Join("themes", "sample", "archetypes"),
|
||||
}
|
||||
for _, dir := range dirs {
|
||||
err = fs.Source.Mkdir(dir, perm)
|
||||
if err != nil {
|
||||
err = fs.Mkdir(dir, perm)
|
||||
if err != nil && !os.IsExist(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -132,6 +183,10 @@ title = "{{ .BaseFileName | upper }}"
|
||||
path: filepath.Join("archetypes", "emptydate.md"),
|
||||
content: "+++\ndate =\"\"\ntitle = \"Empty Date Arch title\"\ntest = \"test1\"\n+++\n",
|
||||
},
|
||||
{
|
||||
path: filepath.Join("archetypes", "lang.md"),
|
||||
content: `Site Lang: {{ .Site.Language.Lang }}|Name: {{ replace .Name "-" " " | title }}|i18n: {{ T "hugo" }}`,
|
||||
},
|
||||
// #3623x
|
||||
{
|
||||
path: filepath.Join("archetypes", "shortcodes.md"),
|
||||
@@ -151,7 +206,7 @@ Some text.
|
||||
`,
|
||||
},
|
||||
} {
|
||||
f, err := fs.Source.Create(v.path)
|
||||
f, err := fs.Create(v.path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -166,8 +221,15 @@ Some text.
|
||||
return nil
|
||||
}
|
||||
|
||||
func assertContains(assert *require.Assertions, v interface{}, matches ...string) {
|
||||
for _, m := range matches {
|
||||
assert.Contains(v, m)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(bep) extract common testing package with this and some others
|
||||
func readFileFromFs(t *testing.T, fs afero.Fs, filename string) string {
|
||||
t.Helper()
|
||||
filename = filepath.FromSlash(filename)
|
||||
b, err := afero.ReadFile(fs, filename)
|
||||
if err != nil {
|
||||
@@ -185,22 +247,39 @@ func readFileFromFs(t *testing.T, fs afero.Fs, filename string) string {
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func newTestCfg() (*viper.Viper, *hugofs.Fs) {
|
||||
func newTestCfg(assert *require.Assertions, mm afero.Fs) (*viper.Viper, *hugofs.Fs) {
|
||||
|
||||
v := viper.New()
|
||||
v.Set("contentDir", "content")
|
||||
v.Set("dataDir", "data")
|
||||
v.Set("i18nDir", "i18n")
|
||||
v.Set("layoutDir", "layouts")
|
||||
v.Set("archetypeDir", "archetypes")
|
||||
v.Set("assetDir", "assets")
|
||||
cfg := `
|
||||
|
||||
fs := hugofs.NewMem(v)
|
||||
theme = "mytheme"
|
||||
[languages]
|
||||
[languages.en]
|
||||
weight = 1
|
||||
languageName = "English"
|
||||
[languages.nn]
|
||||
weight = 2
|
||||
languageName = "Nynorsk"
|
||||
contentDir = "content_nn"
|
||||
|
||||
v.SetFs(fs.Source)
|
||||
`
|
||||
if mm == nil {
|
||||
mm = afero.NewMemMapFs()
|
||||
}
|
||||
|
||||
initViper(v)
|
||||
mm.MkdirAll(filepath.FromSlash("content_nn"), 0777)
|
||||
|
||||
return v, fs
|
||||
mm.MkdirAll(filepath.FromSlash("themes/mytheme"), 0777)
|
||||
|
||||
assert.NoError(afero.WriteFile(mm, filepath.Join("i18n", "en.toml"), []byte(`[hugo]
|
||||
other = "Hugo Rocks!"`), 0755))
|
||||
assert.NoError(afero.WriteFile(mm, filepath.Join("i18n", "nn.toml"), []byte(`[hugo]
|
||||
other = "Hugo Rokkar!"`), 0755))
|
||||
|
||||
assert.NoError(afero.WriteFile(mm, "config.toml", []byte(cfg), 0755))
|
||||
|
||||
v, _, err := hugolib.LoadConfig(hugolib.ConfigSourceDescriptor{Fs: mm, Filename: "config.toml"})
|
||||
assert.NoError(err)
|
||||
|
||||
return v, hugofs.NewFrom(mm, v)
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
// 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.
|
||||
|
||||
package deploy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/cloudfront"
|
||||
)
|
||||
|
||||
// InvalidateCloudFront invalidates the CloudFront cache for distributionID.
|
||||
// It uses the default AWS credentials from the environment.
|
||||
func InvalidateCloudFront(ctx context.Context, distributionID string) error {
|
||||
// SharedConfigEnable enables loading "shared config (~/.aws/config) and
|
||||
// shared credentials (~/.aws/credentials) files".
|
||||
// See https://docs.aws.amazon.com/sdk-for-go/api/aws/session/ for more
|
||||
// details.
|
||||
// This is the same codepath used by Go CDK when creating an s3 URL.
|
||||
// TODO: Update this to a Go CDK helper once available
|
||||
// (https://github.com/google/go-cloud/issues/2003).
|
||||
sess, err := session.NewSessionWithOptions(session.Options{SharedConfigState: session.SharedConfigEnable})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req := &cloudfront.CreateInvalidationInput{
|
||||
DistributionId: aws.String(distributionID),
|
||||
InvalidationBatch: &cloudfront.InvalidationBatch{
|
||||
CallerReference: aws.String(time.Now().Format("20060102150405")),
|
||||
Paths: &cloudfront.Paths{
|
||||
Items: []*string{aws.String("/*")},
|
||||
Quantity: aws.Int64(1),
|
||||
},
|
||||
},
|
||||
}
|
||||
_, err = cloudfront.New(sess).CreateInvalidationWithContext(ctx, req)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,649 @@
|
||||
// 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.
|
||||
|
||||
package deploy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/afero"
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
"golang.org/x/text/unicode/norm"
|
||||
|
||||
"gocloud.dev/blob"
|
||||
_ "gocloud.dev/blob/azureblob" // import
|
||||
_ "gocloud.dev/blob/fileblob" // import
|
||||
_ "gocloud.dev/blob/gcsblob" // import
|
||||
_ "gocloud.dev/blob/s3blob" // import
|
||||
)
|
||||
|
||||
// Deployer supports deploying the site to target cloud providers.
|
||||
type Deployer struct {
|
||||
localFs afero.Fs
|
||||
bucket *blob.Bucket
|
||||
|
||||
target *target // the target to deploy to
|
||||
matchers []*matcher // matchers to apply to uploaded files
|
||||
ordering []*regexp.Regexp // orders uploads
|
||||
quiet bool // true reduces STDOUT
|
||||
confirm bool // true enables confirmation before making changes
|
||||
dryRun bool // true skips conformations and prints changes instead of applying them
|
||||
force bool // true forces upload of all files
|
||||
invalidateCDN bool // true enables invalidate CDN cache (if possible)
|
||||
maxDeletes int // caps the # of files to delete; -1 to disable
|
||||
|
||||
// For tests...
|
||||
summary deploySummary // summary of latest Deploy results
|
||||
}
|
||||
|
||||
type deploySummary struct {
|
||||
NumLocal, NumRemote, NumUploads, NumDeletes int
|
||||
}
|
||||
|
||||
// New constructs a new *Deployer.
|
||||
func New(cfg config.Provider, localFs afero.Fs) (*Deployer, error) {
|
||||
targetName := cfg.GetString("target")
|
||||
|
||||
// Load the [deployment] section of the config.
|
||||
dcfg, err := decodeConfig(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(dcfg.Targets) == 0 {
|
||||
return nil, errors.New("no deployment targets found")
|
||||
}
|
||||
|
||||
// Find the target to deploy to.
|
||||
var tgt *target
|
||||
if targetName == "" {
|
||||
// Default to the first target.
|
||||
tgt = dcfg.Targets[0]
|
||||
} else {
|
||||
for _, t := range dcfg.Targets {
|
||||
if t.Name == targetName {
|
||||
tgt = t
|
||||
}
|
||||
}
|
||||
if tgt == nil {
|
||||
return nil, fmt.Errorf("deployment target %q not found", targetName)
|
||||
}
|
||||
}
|
||||
return &Deployer{
|
||||
localFs: localFs,
|
||||
target: tgt,
|
||||
matchers: dcfg.Matchers,
|
||||
ordering: dcfg.ordering,
|
||||
quiet: cfg.GetBool("quiet"),
|
||||
confirm: cfg.GetBool("confirm"),
|
||||
dryRun: cfg.GetBool("dryRun"),
|
||||
force: cfg.GetBool("force"),
|
||||
invalidateCDN: cfg.GetBool("invalidateCDN"),
|
||||
maxDeletes: cfg.GetInt("maxDeletes"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Deployer) openBucket(ctx context.Context) (*blob.Bucket, error) {
|
||||
if d.bucket != nil {
|
||||
return d.bucket, nil
|
||||
}
|
||||
jww.FEEDBACK.Printf("Deploying to target %q (%s)\n", d.target.Name, d.target.URL)
|
||||
return blob.OpenBucket(ctx, d.target.URL)
|
||||
}
|
||||
|
||||
// Deploy deploys the site to a target.
|
||||
func (d *Deployer) Deploy(ctx context.Context) error {
|
||||
bucket, err := d.openBucket(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Load local files from the source directory.
|
||||
local, err := walkLocal(d.localFs, d.matchers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
jww.INFO.Printf("Found %d local files.\n", len(local))
|
||||
d.summary.NumLocal = len(local)
|
||||
|
||||
// Load remote files from the target.
|
||||
remote, err := walkRemote(ctx, bucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
jww.INFO.Printf("Found %d remote files.\n", len(remote))
|
||||
d.summary.NumRemote = len(remote)
|
||||
|
||||
// Diff local vs remote to see what changes need to be applied.
|
||||
uploads, deletes := findDiffs(local, remote, d.force)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.summary.NumUploads = len(uploads)
|
||||
d.summary.NumDeletes = len(deletes)
|
||||
if len(uploads)+len(deletes) == 0 {
|
||||
if !d.quiet {
|
||||
jww.FEEDBACK.Println("No changes required.")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if !d.quiet {
|
||||
jww.FEEDBACK.Println(summarizeChanges(uploads, deletes))
|
||||
}
|
||||
|
||||
// Ask for confirmation before proceeding.
|
||||
if d.confirm && !d.dryRun {
|
||||
fmt.Printf("Continue? (Y/n) ")
|
||||
var confirm string
|
||||
if _, err := fmt.Scanln(&confirm); err != nil {
|
||||
return err
|
||||
}
|
||||
if confirm != "" && confirm[0] != 'y' && confirm[0] != 'Y' {
|
||||
return errors.New("aborted")
|
||||
}
|
||||
}
|
||||
|
||||
// Order the uploads. They are organized in groups; all uploads in a group
|
||||
// must be complete before moving on to the next group.
|
||||
uploadGroups := applyOrdering(d.ordering, uploads)
|
||||
|
||||
// Apply the changes in parallel, using an inverted worker
|
||||
// pool (https://www.youtube.com/watch?v=5zXAHh5tJqQ&t=26m58s).
|
||||
// sem prevents more than nParallel concurrent goroutines.
|
||||
const nParallel = 10
|
||||
var errs []error
|
||||
var errMu sync.Mutex // protects errs
|
||||
|
||||
for _, uploads := range uploadGroups {
|
||||
// Short-circuit for an empty group.
|
||||
if len(uploads) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Within the group, apply uploads in parallel.
|
||||
sem := make(chan struct{}, nParallel)
|
||||
for _, upload := range uploads {
|
||||
if d.dryRun {
|
||||
if !d.quiet {
|
||||
jww.FEEDBACK.Printf("[DRY RUN] Would upload: %v\n", upload)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
sem <- struct{}{}
|
||||
go func(upload *fileToUpload) {
|
||||
if err := doSingleUpload(ctx, bucket, upload); err != nil {
|
||||
errMu.Lock()
|
||||
defer errMu.Unlock()
|
||||
errs = append(errs, err)
|
||||
}
|
||||
<-sem
|
||||
}(upload)
|
||||
}
|
||||
// Wait for all uploads in the group to finish.
|
||||
for n := nParallel; n > 0; n-- {
|
||||
sem <- struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
if d.maxDeletes != -1 && len(deletes) > d.maxDeletes {
|
||||
jww.WARN.Printf("Skipping %d deletes because it is more than --maxDeletes (%d). If this is expected, set --maxDeletes to a larger number, or -1 to disable this check.\n", len(deletes), d.maxDeletes)
|
||||
d.summary.NumDeletes = 0
|
||||
} else {
|
||||
// Apply deletes in parallel.
|
||||
sort.Slice(deletes, func(i, j int) bool { return deletes[i] < deletes[j] })
|
||||
sem := make(chan struct{}, nParallel)
|
||||
for _, del := range deletes {
|
||||
if d.dryRun {
|
||||
if !d.quiet {
|
||||
jww.FEEDBACK.Printf("[DRY RUN] Would delete %s\n", del)
|
||||
}
|
||||
continue
|
||||
}
|
||||
sem <- struct{}{}
|
||||
go func(del string) {
|
||||
jww.INFO.Printf("Deleting %s...\n", del)
|
||||
if err := bucket.Delete(ctx, del); err != nil {
|
||||
errMu.Lock()
|
||||
defer errMu.Unlock()
|
||||
errs = append(errs, err)
|
||||
}
|
||||
<-sem
|
||||
}(del)
|
||||
}
|
||||
// Wait for all deletes to finish.
|
||||
for n := nParallel; n > 0; n-- {
|
||||
sem <- struct{}{}
|
||||
}
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
if !d.quiet {
|
||||
jww.FEEDBACK.Printf("Encountered %d errors.\n", len(errs))
|
||||
}
|
||||
return errs[0]
|
||||
}
|
||||
if !d.quiet {
|
||||
jww.FEEDBACK.Println("Success!")
|
||||
}
|
||||
|
||||
if d.invalidateCDN && d.target.CloudFrontDistributionID != "" {
|
||||
jww.FEEDBACK.Println("Invalidating CloudFront CDN...")
|
||||
if err := InvalidateCloudFront(ctx, d.target.CloudFrontDistributionID); err != nil {
|
||||
jww.FEEDBACK.Printf("Failed to invalidate CloudFront CDN: %v\n", err)
|
||||
return err
|
||||
}
|
||||
jww.FEEDBACK.Println("Success!")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// summarizeChanges creates a text description of the proposed changes.
|
||||
func summarizeChanges(uploads []*fileToUpload, deletes []string) string {
|
||||
uploadSize := int64(0)
|
||||
for _, u := range uploads {
|
||||
uploadSize += u.Local.UploadSize
|
||||
}
|
||||
return fmt.Sprintf("Identified %d file(s) to upload, totaling %s, and %d file(s) to delete.", len(uploads), humanize.Bytes(uint64(uploadSize)), len(deletes))
|
||||
}
|
||||
|
||||
// doSingleUpload executes a single file upload.
|
||||
func doSingleUpload(ctx context.Context, bucket *blob.Bucket, upload *fileToUpload) error {
|
||||
jww.INFO.Printf("Uploading %v...\n", upload)
|
||||
opts := &blob.WriterOptions{
|
||||
CacheControl: upload.Local.CacheControl(),
|
||||
ContentEncoding: upload.Local.ContentEncoding(),
|
||||
ContentType: upload.Local.ContentType(),
|
||||
}
|
||||
w, err := bucket.NewWriter(ctx, upload.Local.SlashPath, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r, err := upload.Local.Reader()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Close()
|
||||
_, err = io.Copy(w, r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// localFile represents a local file from the source. Use newLocalFile to
|
||||
// construct one.
|
||||
type localFile struct {
|
||||
// NativePath is the native path to the file (using file.Separator).
|
||||
NativePath string
|
||||
// SlashPath is NativePath converted to use /.
|
||||
SlashPath string
|
||||
// UploadSize is the size of the content to be uploaded. It may not
|
||||
// be the same as the local file size if the content will be
|
||||
// gzipped before upload.
|
||||
UploadSize int64
|
||||
|
||||
fs afero.Fs
|
||||
matcher *matcher
|
||||
md5 []byte // cache
|
||||
gzipped bytes.Buffer // cached of gzipped contents if gzipping
|
||||
}
|
||||
|
||||
// newLocalFile initializes a *localFile.
|
||||
func newLocalFile(fs afero.Fs, nativePath, slashpath string, m *matcher) (*localFile, error) {
|
||||
f, err := fs.Open(nativePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
lf := &localFile{
|
||||
NativePath: nativePath,
|
||||
SlashPath: slashpath,
|
||||
fs: fs,
|
||||
matcher: m,
|
||||
}
|
||||
if m != nil && m.Gzip {
|
||||
// We're going to gzip the content. Do it once now, and cache the result
|
||||
// in gzipped. The UploadSize is the size of the gzipped content.
|
||||
gz := gzip.NewWriter(&lf.gzipped)
|
||||
if _, err := io.Copy(gz, f); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := gz.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lf.UploadSize = int64(lf.gzipped.Len())
|
||||
} else {
|
||||
// Raw content. Just get the UploadSize.
|
||||
info, err := f.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lf.UploadSize = info.Size()
|
||||
}
|
||||
return lf, nil
|
||||
}
|
||||
|
||||
// Reader returns an io.ReadCloser for reading the content to be uploaded.
|
||||
// The caller must call Close on the returned ReaderCloser.
|
||||
// The reader content may not be the same as the local file content due to
|
||||
// gzipping.
|
||||
func (lf *localFile) Reader() (io.ReadCloser, error) {
|
||||
if lf.matcher != nil && lf.matcher.Gzip {
|
||||
// We've got the gzipped contents cached in gzipped.
|
||||
// Note: we can't use lf.gzipped directly as a Reader, since we it discards
|
||||
// data after it is read, and we may read it more than once.
|
||||
return ioutil.NopCloser(bytes.NewReader(lf.gzipped.Bytes())), nil
|
||||
}
|
||||
// Not expected to fail since we did it successfully earlier in newLocalFile,
|
||||
// but could happen due to changes in the underlying filesystem.
|
||||
return lf.fs.Open(lf.NativePath)
|
||||
}
|
||||
|
||||
// CacheControl returns the Cache-Control header to use for lf, based on the
|
||||
// first matching matcher (if any).
|
||||
func (lf *localFile) CacheControl() string {
|
||||
if lf.matcher == nil {
|
||||
return ""
|
||||
}
|
||||
return lf.matcher.CacheControl
|
||||
}
|
||||
|
||||
// ContentEncoding returns the Content-Encoding header to use for lf, based
|
||||
// on the matcher's Content-Encoding and Gzip fields.
|
||||
func (lf *localFile) ContentEncoding() string {
|
||||
if lf.matcher == nil {
|
||||
return ""
|
||||
}
|
||||
if lf.matcher.Gzip {
|
||||
return "gzip"
|
||||
}
|
||||
return lf.matcher.ContentEncoding
|
||||
}
|
||||
|
||||
// ContentType returns the Content-Type header to use for lf.
|
||||
// It first checks if there's a Content-Type header configured via a matching
|
||||
// matcher; if not, it tries to generate one based on the filename extension.
|
||||
// If this fails, the Content-Type will be the empty string. In this case, Go
|
||||
// Cloud will automatically try to infer a Content-Type based on the file
|
||||
// content.
|
||||
func (lf *localFile) ContentType() string {
|
||||
if lf.matcher != nil && lf.matcher.ContentType != "" {
|
||||
return lf.matcher.ContentType
|
||||
}
|
||||
// TODO: Hugo has a MediaType and a MediaTypes list and also a concept
|
||||
// of custom MIME types.
|
||||
// Use 1) The matcher 2) Hugo's MIME types 3) TypeByExtension.
|
||||
return mime.TypeByExtension(filepath.Ext(lf.NativePath))
|
||||
}
|
||||
|
||||
// Force returns true if the file should be forced to re-upload based on the
|
||||
// matching matcher.
|
||||
func (lf *localFile) Force() bool {
|
||||
return lf.matcher != nil && lf.matcher.Force
|
||||
}
|
||||
|
||||
// MD5 returns an MD5 hash of the content to be uploaded.
|
||||
func (lf *localFile) MD5() []byte {
|
||||
if len(lf.md5) > 0 {
|
||||
return lf.md5
|
||||
}
|
||||
h := md5.New()
|
||||
r, err := lf.Reader()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer r.Close()
|
||||
if _, err := io.Copy(h, r); err != nil {
|
||||
return nil
|
||||
}
|
||||
lf.md5 = h.Sum(nil)
|
||||
return lf.md5
|
||||
}
|
||||
|
||||
// walkLocal walks the source directory and returns a flat list of files,
|
||||
// using localFile.SlashPath as the map keys.
|
||||
func walkLocal(fs afero.Fs, matchers []*matcher) (map[string]*localFile, error) {
|
||||
retval := map[string]*localFile{}
|
||||
err := afero.Walk(fs, "", func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.IsDir() {
|
||||
// Skip hidden directories.
|
||||
if path != "" && strings.HasPrefix(info.Name(), ".") {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// .DS_Store is an internal MacOS attribute file; skip it.
|
||||
if info.Name() == ".DS_Store" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// When a file system is HFS+, its filepath is in NFD form.
|
||||
if runtime.GOOS == "darwin" {
|
||||
path = norm.NFC.String(path)
|
||||
}
|
||||
|
||||
// Find the first matching matcher (if any).
|
||||
slashpath := filepath.ToSlash(path)
|
||||
var m *matcher
|
||||
for _, cur := range matchers {
|
||||
if cur.Matches(slashpath) {
|
||||
m = cur
|
||||
break
|
||||
}
|
||||
}
|
||||
lf, err := newLocalFile(fs, path, slashpath, m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
retval[lf.SlashPath] = lf
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return retval, nil
|
||||
}
|
||||
|
||||
// walkRemote walks the target bucket and returns a flat list.
|
||||
func walkRemote(ctx context.Context, bucket *blob.Bucket) (map[string]*blob.ListObject, error) {
|
||||
retval := map[string]*blob.ListObject{}
|
||||
iter := bucket.List(nil)
|
||||
for {
|
||||
obj, err := iter.Next(ctx)
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// If the remote didn't give us an MD5, compute one.
|
||||
// This can happen for some providers (e.g., fileblob, which uses the
|
||||
// local filesystem), but not for the most common Cloud providers
|
||||
// (S3, GCS, Azure). Although, it can happen for S3 if the blob was uploaded
|
||||
// via a multi-part upload.
|
||||
// Although it's unfortunate to have to read the file, it's likely better
|
||||
// than assuming a delta and re-uploading it.
|
||||
if len(obj.MD5) == 0 {
|
||||
r, err := bucket.NewReader(ctx, obj.Key, nil)
|
||||
if err == nil {
|
||||
h := md5.New()
|
||||
if _, err := io.Copy(h, r); err == nil {
|
||||
obj.MD5 = h.Sum(nil)
|
||||
}
|
||||
r.Close()
|
||||
}
|
||||
}
|
||||
retval[obj.Key] = obj
|
||||
}
|
||||
return retval, nil
|
||||
}
|
||||
|
||||
// uploadReason is an enum of reasons why a file must be uploaded.
|
||||
type uploadReason string
|
||||
|
||||
const (
|
||||
reasonUnknown uploadReason = "unknown"
|
||||
reasonNotFound uploadReason = "not found at target"
|
||||
reasonForce uploadReason = "--force"
|
||||
reasonSize uploadReason = "size differs"
|
||||
reasonMD5Differs uploadReason = "md5 differs"
|
||||
reasonMD5Missing uploadReason = "remote md5 missing"
|
||||
)
|
||||
|
||||
// fileToUpload represents a single local file that should be uploaded to
|
||||
// the target.
|
||||
type fileToUpload struct {
|
||||
Local *localFile
|
||||
Reason uploadReason
|
||||
}
|
||||
|
||||
func (u *fileToUpload) String() string {
|
||||
details := []string{humanize.Bytes(uint64(u.Local.UploadSize))}
|
||||
if s := u.Local.CacheControl(); s != "" {
|
||||
details = append(details, fmt.Sprintf("Cache-Control: %q", s))
|
||||
}
|
||||
if s := u.Local.ContentEncoding(); s != "" {
|
||||
details = append(details, fmt.Sprintf("Content-Encoding: %q", s))
|
||||
}
|
||||
if s := u.Local.ContentType(); s != "" {
|
||||
details = append(details, fmt.Sprintf("Content-Type: %q", s))
|
||||
}
|
||||
return fmt.Sprintf("%s (%s): %v", u.Local.SlashPath, strings.Join(details, ", "), u.Reason)
|
||||
}
|
||||
|
||||
// findDiffs diffs localFiles vs remoteFiles to see what changes should be
|
||||
// applied to the remote target. It returns a slice of *fileToUpload and a
|
||||
// slice of paths for files to delete.
|
||||
func findDiffs(localFiles map[string]*localFile, remoteFiles map[string]*blob.ListObject, force bool) ([]*fileToUpload, []string) {
|
||||
var uploads []*fileToUpload
|
||||
var deletes []string
|
||||
|
||||
found := map[string]bool{}
|
||||
for path, lf := range localFiles {
|
||||
upload := false
|
||||
reason := reasonUnknown
|
||||
|
||||
if remoteFile, ok := remoteFiles[path]; ok {
|
||||
// The file exists in remote. Let's see if we need to upload it anyway.
|
||||
|
||||
// TODO: We don't register a diff if the metadata (e.g., Content-Type
|
||||
// header) has changed. This would be difficult/expensive to detect; some
|
||||
// providers return metadata along with their "List" result, but others
|
||||
// (notably AWS S3) do not, so gocloud.dev's blob.Bucket doesn't expose
|
||||
// it in the list result. It would require a separate request per blob
|
||||
// to fetch. At least for now, we work around this by documenting it and
|
||||
// providing a "force" flag (to re-upload everything) and a "force" bool
|
||||
// per matcher (to re-upload all files in a matcher whose headers may have
|
||||
// changed).
|
||||
// Idea: extract a sample set of 1 file per extension + 1 file per matcher
|
||||
// and check those files?
|
||||
if force {
|
||||
upload = true
|
||||
reason = reasonForce
|
||||
} else if lf.Force() {
|
||||
upload = true
|
||||
reason = reasonForce
|
||||
} else if lf.UploadSize != remoteFile.Size {
|
||||
upload = true
|
||||
reason = reasonSize
|
||||
} else if len(remoteFile.MD5) == 0 {
|
||||
// This shouldn't happen unless the remote didn't give us an MD5 hash
|
||||
// from List, AND we failed to compute one by reading the remote file.
|
||||
// Default to considering the files different.
|
||||
upload = true
|
||||
reason = reasonMD5Missing
|
||||
} else if !bytes.Equal(lf.MD5(), remoteFile.MD5) {
|
||||
upload = true
|
||||
reason = reasonMD5Differs
|
||||
} else {
|
||||
// Nope! Leave uploaded = false.
|
||||
}
|
||||
found[path] = true
|
||||
} else {
|
||||
// The file doesn't exist in remote.
|
||||
upload = true
|
||||
reason = reasonNotFound
|
||||
}
|
||||
if upload {
|
||||
jww.DEBUG.Printf("%s needs to be uploaded: %v\n", path, reason)
|
||||
uploads = append(uploads, &fileToUpload{lf, reason})
|
||||
} else {
|
||||
jww.DEBUG.Printf("%s exists at target and does not need to be uploaded", path)
|
||||
}
|
||||
}
|
||||
|
||||
// Remote files that weren't found locally should be deleted.
|
||||
for path := range remoteFiles {
|
||||
if !found[path] {
|
||||
deletes = append(deletes, path)
|
||||
}
|
||||
}
|
||||
return uploads, deletes
|
||||
}
|
||||
|
||||
// applyOrdering returns an ordered slice of slices of uploads.
|
||||
//
|
||||
// The returned slice will have length len(ordering)+1.
|
||||
//
|
||||
// The subslice at index i, for i = 0 ... len(ordering)-1, will have all of the
|
||||
// uploads whose Local.SlashPath matched the regex at ordering[i] (but not any
|
||||
// previous ordering regex).
|
||||
// The subslice at index len(ordering) will have the remaining uploads that
|
||||
// didn't match any ordering regex.
|
||||
//
|
||||
// The subslices are sorted by Local.SlashPath.
|
||||
func applyOrdering(ordering []*regexp.Regexp, uploads []*fileToUpload) [][]*fileToUpload {
|
||||
|
||||
// Sort the whole slice by Local.SlashPath first.
|
||||
sort.Slice(uploads, func(i, j int) bool { return uploads[i].Local.SlashPath < uploads[j].Local.SlashPath })
|
||||
|
||||
retval := make([][]*fileToUpload, len(ordering)+1)
|
||||
for _, u := range uploads {
|
||||
matched := false
|
||||
for i, re := range ordering {
|
||||
if re.MatchString(u.Local.SlashPath) {
|
||||
retval[i] = append(retval[i], u)
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
retval[len(ordering)] = append(retval[len(ordering)], u)
|
||||
}
|
||||
}
|
||||
return retval
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user