From d171e11028f5993137a5f83beb7fe002bed866f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Fri, 26 Jun 2020 19:25:17 +0200 Subject: [PATCH 01/25] repository: partial impl of a go-git backed Repo --- go.mod | 3 +- go.sum | 113 ++++++----- repository/gogit.go | 378 ++++++++++++++++++++++++++++++++++++ repository/gogit_config.go | 175 +++++++++++++++++ repository/gogit_test.go | 68 +++++++ repository/gogit_testing.go | 58 ++++++ 6 files changed, 735 insertions(+), 60 deletions(-) create mode 100644 repository/gogit.go create mode 100644 repository/gogit_config.go create mode 100644 repository/gogit_test.go create mode 100644 repository/gogit_testing.go diff --git a/go.mod b/go.mod index 4177485a..9f8b259b 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/corpix/uarand v0.1.1 // indirect github.com/dustin/go-humanize v1.0.0 github.com/fatih/color v1.9.0 + github.com/go-git/go-git/v5 v5.1.0 github.com/gorilla/mux v1.8.0 github.com/hashicorp/golang-lru v0.5.4 github.com/icrowley/fake v0.0.0-20180203215853-4178557ae428 @@ -25,7 +26,7 @@ require ( github.com/stretchr/testify v1.6.1 github.com/vektah/gqlparser v1.3.1 github.com/xanzy/go-gitlab v0.33.0 - golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 + golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288 golang.org/x/sync v0.0.0-20190423024810-112230192c58 golang.org/x/text v0.3.3 diff --git a/go.sum b/go.sum index f818acca..9b84ea6d 100644 --- a/go.sum +++ b/go.sum @@ -1,32 +1,30 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -github.com/99designs/gqlgen v0.10.3-0.20200208093655-ab8d62b67dd0 h1:ADy3XJwhOYg6Pb90XeXazWvO+9gpOsgLuaM1buZUZOY= -github.com/99designs/gqlgen v0.10.3-0.20200208093655-ab8d62b67dd0/go.mod h1:dfBhwZKMcSYiYRMTs8qWF+Oha6782e1xPfgRmVal9I8= github.com/99designs/gqlgen v0.10.3-0.20200209012558-b7a58a1c0e4b h1:510xa84qGbDemwTHNio4cLWkdKFxxJgVtsIOH+Ku8bo= github.com/99designs/gqlgen v0.10.3-0.20200209012558-b7a58a1c0e4b/go.mod h1:dfBhwZKMcSYiYRMTs8qWF+Oha6782e1xPfgRmVal9I8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Masterminds/glide v0.13.2/go.mod h1:STyF5vcenH/rUqTEv+/hBXlSTo7KYwg2oc2f4tzPWic= github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/vcs v1.13.0/go.mod h1:N09YCmOQr6RLxC6UNHzuVwAdodYbbnycGHSmwVJjcKA= -github.com/MichaelMure/go-term-text v0.2.6 h1:dSmJSzk2iI5xWymSMrMbdVM1bxYWu3DjDFhdcJvAuqA= -github.com/MichaelMure/go-term-text v0.2.6/go.mod h1:o2Z5T3b28F4kwAojGvvNdbzjHf9t18vbQ7E2pmTe2Ww= -github.com/MichaelMure/go-term-text v0.2.7 h1:nSYvYGwXxJoiQu6kdGSErpxZ6ah/4WlJyp/niqQor6g= -github.com/MichaelMure/go-term-text v0.2.7/go.mod h1:6z+q5b/nP1V8I9KkWQcUi5QpmF8DVrz9vLJ4hdoxHnM= -github.com/MichaelMure/go-term-text v0.2.8 h1:daXIVPjPkAhcLhA+tfjQBHYjatb1D42/LY1Nw2PXYlU= -github.com/MichaelMure/go-term-text v0.2.8/go.mod h1:6z+q5b/nP1V8I9KkWQcUi5QpmF8DVrz9vLJ4hdoxHnM= github.com/MichaelMure/go-term-text v0.2.9 h1:jUxInT3rDhl4WoJgLnmMS3hR79zigyJS1TqKFDTI6xE= github.com/MichaelMure/go-term-text v0.2.9/go.mod h1:2QSU/Nn2u41Tqoar+90RlYuhjngJPYgod7evnsYwkWc= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/agnivade/levenshtein v1.0.1 h1:3oJU7J3FGFmyhn8KHjmVaZCN5hxTr7GxgRue+sxIXdQ= github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= +github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs= +github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/araddon/dateparse v0.0.0-20190622164848-0fb0a474d195 h1:c4mLfegoDw6OhSJXTd2jUEQgZUQuJWtocudb97Qn9EM= github.com/araddon/dateparse v0.0.0-20190622164848-0fb0a474d195/go.mod h1:SLqhdZcd+dF3TEVL2RMoob5bBP5R1P1qkox+HtCBgGI= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/awesome-gocui/gocui v0.6.1-0.20191115151952-a34ffb055986 h1:QvIfX96O11qjX1Zr3hKkG0dI12JBRBGABWffyZ1GI60= github.com/awesome-gocui/gocui v0.6.1-0.20191115151952-a34ffb055986/go.mod h1:1QikxFaPhe2frKeKvEwZEIGia3haiOxOUXKinrv17mA= github.com/awesome-gocui/termbox-go v0.0.0-20190427202837-c0aef3d18bcc h1:wGNpKcHU8Aadr9yOzsT3GEsFLS7HQu8HxQIomnekqf0= @@ -45,7 +43,6 @@ github.com/coreos/bbolt v1.3.2 h1:wZwiHHUieZCquLkDL0B8UhzreNWsPHooDAG3q34zk0s= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible h1:jFneRYjIvLMLhDLCzuTuU4rSJUjRplcJQ7pD7MnhC04= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e h1:Wf6HqHfScWJN9/ZjdUKyjop4mf3Qdd+1TvvltAvM3m8= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= @@ -53,29 +50,37 @@ github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f h1:lBNOc5arjvs8E5mO2tbp github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/corpix/uarand v0.1.1 h1:RMr1TWc9F4n5jiPDzFHtmaUXLKLNUFK0SgCLo4BhX/U= github.com/corpix/uarand v0.1.1/go.mod h1:SFKZvkcRoLqVRFZ4u25xPmp6m9ktANfbpXZ7SJ0/FNU= -github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= -github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/dustin/go-humanize v0.0.0-20180713052910-9f541cc9db5d/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= +github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= +github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= -github.com/go-errors/errors v1.0.2 h1:xMxH9j2fNg/L4hLn/4y3M0IUsn0M6Wbu/Uh9QlOfBh4= -github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs= -github.com/go-errors/errors v1.1.1 h1:ljK/pL5ltg3qoN+OtN6yCv9HWSfMwxSx90GJCZQxYNg= -github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs= +github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= +github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= +github.com/go-git/go-billy/v5 v5.0.0 h1:7NQHvd9FVid8VL4qVUMm8XifBK+2xCoZ2lSk0agRrHM= +github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= +github.com/go-git/go-git-fixtures/v4 v4.0.1 h1:q+IFMfLx200Q3scvt2hN79JsEzy4AmBTp/pqnefH+Bc= +github.com/go-git/go-git-fixtures/v4 v4.0.1/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw= +github.com/go-git/go-git/v5 v5.1.0 h1:HxJn9g/E7eYvKW3Fm7Jt4ee8LXfPOm/H1cdDu8vEssk= +github.com/go-git/go-git/v5 v5.1.0/go.mod h1:ZKfuPUoY1ZqIG4QG9BDBh3G4gLM5zvPuSJAozQrZuyM= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= @@ -92,14 +97,12 @@ github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= -github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= -github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.2.0 h1:VJtLvh6VQym50czpZzx07z/kw9EgAxI3x1ZB8taTMQQ= @@ -122,12 +125,17 @@ github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uG github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/icrowley/fake v0.0.0-20180203215853-4178557ae428 h1:Mo9W14pwbO9VfRe+ygqZ8dFbPpoIK1HFrG/zjTuQ+nc= github.com/icrowley/fake v0.0.0-20180203215853-4178557ae428/go.mod h1:uhpZMVGznybq1itEKXj6RYw9I71qK4kH+OGMjRC4KEo= +github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg= +github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/jondot/goweight v1.0.5 h1:aRpnyj1G8BLLNhem8xezuuV0GlFz4G11e3/UtBU/FlQ= -github.com/jondot/goweight v1.0.5/go.mod h1:3PRcpOwkyspe1t4+KCNgauas+aNDTSSCwZ6AQ4kDD/A= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY= +github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -137,6 +145,8 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ= @@ -148,22 +158,19 @@ github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOA github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/mattn/go-runewidth v0.0.6 h1:V2iyH+aX9C5fsYCpK60U8BYIvmhqxuOL3JZcqc1NB7k= -github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.8 h1:3tS41NlGYSmhhe/8fhGRzc+z3AYCw1Fe1WAyLuujKs0= -github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-zglob v0.0.0-20180803001819-2ea3427bfa53 h1:tGfIHhDghvEnneeRhODvGYOt305TPwingKt6p90F4MU= -github.com/mattn/go-zglob v0.0.0-20180803001819-2ea3427bfa53/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/ngdinhtoan/glide-cleanup v0.2.0/go.mod h1:UQzsmiDOb8YV3nOsCxK/c9zPpCZVNoHScRE3EO9pVMM= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= @@ -192,12 +199,12 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= -github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= -github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shurcooL/githubv4 v0.0.0-20190601194912-068505affed7 h1:Vk3RiBQpF0Ja+OqbFG7lYTk79+l8Cm2QESLXB0x6u6U= github.com/shurcooL/githubv4 v0.0.0-20190601194912-068505affed7/go.mod h1:hAF0iLZy4td2EX+/8Tw+4nodhlMrwN3HupfaXj3zkGo= github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f h1:tygelZueB1EtXkPI6mQ4o9DQ0+FKW41hTbunoXZCTqk= @@ -215,19 +222,11 @@ github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4k github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= -github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= -github.com/spf13/cobra v0.0.6 h1:breEStsVwemnKh2/s6gMvSdMEkwW0sK8vGStnlVBMCs= -github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= -github.com/spf13/cobra v0.0.7 h1:FfTH+vuMXOas8jmfb5/M7dzEYx7LpcLb7a0LPe34uOU= -github.com/spf13/cobra v0.0.7/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -242,35 +241,18 @@ github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/theckman/goconstraint v1.11.0 h1:oBUwN5wpE4dwyPhRGraEgJsFTr+JtLWiDnaJZJeeXI0= -github.com/theckman/goconstraint v1.11.0/go.mod h1:zkCR/f2kOULTk/h1ujgyB9BlCNLaqlQ6GN2Zl4mg81g= -github.com/thoas/go-funk v0.0.0-20180716193722-1060394a7713 h1:knaxjm6QMbUMNvuaSnJZmw0gRX4V/79JVUQiziJGM84= -github.com/thoas/go-funk v0.0.0-20180716193722-1060394a7713/go.mod h1:mlR+dHGb+4YgXkf13rkQTuzrneeHANxOm6+ZnEV9HsA= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= -github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U= -github.com/vektah/gqlparser v1.2.1 h1:C+L7Go/eUbN0w6Y0kaiq2W6p2wN5j8wU82EdDXxDivc= -github.com/vektah/gqlparser v1.2.1/go.mod h1:bkVf0FX+Stjg/MHnm8mEyubuaArhNEqfQhF+OTiAL74= github.com/vektah/gqlparser v1.3.1 h1:8b0IcD3qZKWJQHSzynbDlrtP3IxVydZ2DZepCGofqfU= github.com/vektah/gqlparser v1.3.1 h1:8b0IcD3qZKWJQHSzynbDlrtP3IxVydZ2DZepCGofqfU= github.com/vektah/gqlparser v1.3.1/go.mod h1:bkVf0FX+Stjg/MHnm8mEyubuaArhNEqfQhF+OTiAL74= github.com/vektah/gqlparser v1.3.1/go.mod h1:bkVf0FX+Stjg/MHnm8mEyubuaArhNEqfQhF+OTiAL74= -github.com/xanzy/go-gitlab v0.22.1 h1:TVxgHmoa35jQL+9FCkG0nwPDxU9dQZXknBTDtGaSFno= -github.com/xanzy/go-gitlab v0.22.1/go.mod h1:t4Bmvnxj7k37S4Y17lfLx+nLqkf/oQwT2HagfWKv5Og= -github.com/xanzy/go-gitlab v0.24.0 h1:zP1zC4K76Gha0coN5GhygOLhsHTCvUjrnqGL3kHXkVU= -github.com/xanzy/go-gitlab v0.24.0/go.mod h1:t4Bmvnxj7k37S4Y17lfLx+nLqkf/oQwT2HagfWKv5Og= -github.com/xanzy/go-gitlab v0.25.0 h1:G5aTZeqZd66Q6qMVieBfmHBsPpF0jY92zCLAMpULe3I= -github.com/xanzy/go-gitlab v0.25.0/go.mod h1:t4Bmvnxj7k37S4Y17lfLx+nLqkf/oQwT2HagfWKv5Og= -github.com/xanzy/go-gitlab v0.26.0 h1:eAnJRBUC+GDJSy8OoGCZBqBMpXsGOOT235TFm/F8C0Q= -github.com/xanzy/go-gitlab v0.26.0/go.mod h1:t4Bmvnxj7k37S4Y17lfLx+nLqkf/oQwT2HagfWKv5Og= -github.com/xanzy/go-gitlab v0.27.0 h1:zy7xBB8+PID6izH07ZArtkEisJ192dtQajRaeo4+glg= -github.com/xanzy/go-gitlab v0.27.0/go.mod h1:t4Bmvnxj7k37S4Y17lfLx+nLqkf/oQwT2HagfWKv5Og= -github.com/xanzy/go-gitlab v0.29.0 h1:9tMvAkG746eIlzcdpnRgpcKPA1woUDmldMIjR/E5OWM= -github.com/xanzy/go-gitlab v0.29.0/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug= github.com/xanzy/go-gitlab v0.33.0 h1:MUJZknbLhVXSFzBA5eqGGhQ2yHSu8tPbGBPeB3sN4B0= github.com/xanzy/go-gitlab v0.33.0/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug= +github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70= +github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= @@ -278,11 +260,13 @@ go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= @@ -297,6 +281,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288 h1:JIqe8uIcRBHXDQVvZtHwp80ai3Lw3IJAeJEs55Dc1W0= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -309,9 +295,9 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= @@ -319,6 +305,8 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= @@ -348,11 +336,18 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/repository/gogit.go b/repository/gogit.go new file mode 100644 index 00000000..71a7e6d0 --- /dev/null +++ b/repository/gogit.go @@ -0,0 +1,378 @@ +package repository + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + stdpath "path" + "path/filepath" + "sync" + + gogit "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/config" + "github.com/go-git/go-git/v5/plumbing" + + "github.com/MichaelMure/git-bug/util/lamport" +) + +var _ ClockedRepo = &GoGitRepo{} + +type GoGitRepo struct { + r *gogit.Repository + path string + + clocksMutex sync.Mutex + clocks map[string]lamport.Clock +} + +func NewGoGitRepo(path string, clockLoaders []ClockLoader) (*GoGitRepo, error) { + path, err := detectGitPath(path) + if err != nil { + return nil, err + } + + r, err := gogit.PlainOpen(path) + if err != nil { + return nil, err + } + + repo := &GoGitRepo{ + r: r, + path: path, + clocks: make(map[string]lamport.Clock), + } + + for _, loader := range clockLoaders { + allExist := true + for _, name := range loader.Clocks { + if _, err := repo.getClock(name); err != nil { + allExist = false + } + } + + if !allExist { + err = loader.Witnesser(repo) + if err != nil { + return nil, err + } + } + } + + return repo, nil +} + +func detectGitPath(path string) (string, error) { + // normalize the path + path, err := filepath.Abs(path) + if err != nil { + return "", err + } + + for { + fi, err := os.Stat(stdpath.Join(path, ".git")) + if err == nil { + if !fi.IsDir() { + return "", fmt.Errorf(".git exist but is not a directory") + } + return stdpath.Join(path, ".git"), nil + } + if !os.IsNotExist(err) { + // unknown error + return "", err + } + + // detect bare repo + ok, err := isGitDir(path) + if err != nil { + return "", err + } + if ok { + return path, nil + } + + if parent := filepath.Dir(path); parent == path { + return "", fmt.Errorf(".git not found") + } else { + path = parent + } + } +} + +func isGitDir(path string) (bool, error) { + markers := []string{"HEAD", "objects", "refs"} + + for _, marker := range markers { + _, err := os.Stat(stdpath.Join(path, marker)) + if err == nil { + continue + } + if !os.IsNotExist(err) { + // unknown error + return false, err + } else { + return false, nil + } + } + + return true, nil +} + +// InitGoGitRepo create a new empty git repo at the given path +func InitGoGitRepo(path string) (*GoGitRepo, error) { + r, err := gogit.PlainInit(path, false) + if err != nil { + return nil, err + } + + return &GoGitRepo{ + r: r, + path: path + "/.git", + clocks: make(map[string]lamport.Clock), + }, nil +} + +// InitBareGoGitRepo create a new --bare empty git repo at the given path +func InitBareGoGitRepo(path string) (*GoGitRepo, error) { + r, err := gogit.PlainInit(path, true) + if err != nil { + return nil, err + } + + return &GoGitRepo{ + r: r, + path: path, + clocks: make(map[string]lamport.Clock), + }, nil +} + +func (repo *GoGitRepo) LocalConfig() Config { + return newGoGitConfig(repo.r) +} + +func (repo *GoGitRepo) GlobalConfig() Config { + panic("go-git doesn't support writing global config") +} + +// GetPath returns the path to the repo. +func (repo *GoGitRepo) GetPath() string { + return repo.path +} + +// GetUserName returns the name the the user has used to configure git +func (repo *GoGitRepo) GetUserName() (string, error) { + cfg, err := repo.r.Config() + if err != nil { + return "", err + } + + return cfg.User.Name, nil +} + +// GetUserEmail returns the email address that the user has used to configure git. +func (repo *GoGitRepo) GetUserEmail() (string, error) { + cfg, err := repo.r.Config() + if err != nil { + return "", err + } + + return cfg.User.Email, nil +} + +// GetCoreEditor returns the name of the editor that the user has used to configure git. +func (repo *GoGitRepo) GetCoreEditor() (string, error) { + + panic("implement me") +} + +// GetRemotes returns the configured remotes repositories. +func (repo *GoGitRepo) GetRemotes() (map[string]string, error) { + cfg, err := repo.r.Config() + if err != nil { + return nil, err + } + + result := make(map[string]string, len(cfg.Remotes)) + for name, remote := range cfg.Remotes { + if len(remote.URLs) > 0 { + result[name] = remote.URLs[0] + } + } + + return result, nil +} + +// FetchRefs fetch git refs from a remote +func (repo *GoGitRepo) FetchRefs(remote string, refSpec string) (string, error) { + buf := bytes.NewBuffer(nil) + + err := repo.r.Fetch(&gogit.FetchOptions{ + RemoteName: remote, + RefSpecs: []config.RefSpec{config.RefSpec(refSpec)}, + Progress: buf, + }) + if err != nil { + return "", err + } + + return buf.String(), nil +} + +// PushRefs push git refs to a remote +func (repo *GoGitRepo) PushRefs(remote string, refSpec string) (string, error) { + buf := bytes.NewBuffer(nil) + + err := repo.r.Push(&gogit.PushOptions{ + RemoteName: remote, + RefSpecs: []config.RefSpec{config.RefSpec(refSpec)}, + Progress: buf, + }) + if err != nil { + return "", err + } + + return buf.String(), nil +} + +// StoreData will store arbitrary data and return the corresponding hash +func (repo *GoGitRepo) StoreData(data []byte) (Hash, error) { + obj := repo.r.Storer.NewEncodedObject() + obj.SetType(plumbing.BlobObject) + + w, err := obj.Writer() + if err != nil { + return "", err + } + + _, err = w.Write(data) + if err != nil { + return "", err + } + + h, err := repo.r.Storer.SetEncodedObject(obj) + if err != nil { + return "", err + } + + return Hash(h.String()), nil +} + +// ReadData will attempt to read arbitrary data from the given hash +func (repo *GoGitRepo) ReadData(hash Hash) ([]byte, error) { + obj, err := repo.r.BlobObject(plumbing.NewHash(hash.String())) + if err != nil { + return nil, err + } + + r, err := obj.Reader() + if err != nil { + return nil, err + } + + return ioutil.ReadAll(r) +} + +func (repo *GoGitRepo) StoreTree(mapping []TreeEntry) (Hash, error) { + panic("implement me") +} + +func (repo *GoGitRepo) ReadTree(hash Hash) ([]TreeEntry, error) { + panic("implement me") +} + +func (repo *GoGitRepo) StoreCommit(treeHash Hash) (Hash, error) { + panic("implement me") +} + +func (repo *GoGitRepo) StoreCommitWithParent(treeHash Hash, parent Hash) (Hash, error) { + panic("implement me") +} + +func (repo *GoGitRepo) GetTreeHash(commit Hash) (Hash, error) { + panic("implement me") +} + +func (repo *GoGitRepo) FindCommonAncestor(commit1 Hash, commit2 Hash) (Hash, error) { + panic("implement me") +} + +func (repo *GoGitRepo) UpdateRef(ref string, hash Hash) error { + panic("implement me") +} + +func (repo *GoGitRepo) RemoveRef(ref string) error { + panic("implement me") +} + +func (repo *GoGitRepo) ListRefs(refspec string) ([]string, error) { + panic("implement me") +} + +func (repo *GoGitRepo) RefExist(ref string) (bool, error) { + panic("implement me") +} + +func (repo *GoGitRepo) CopyRef(source string, dest string) error { + panic("implement me") +} + +func (repo *GoGitRepo) ListCommits(ref string) ([]Hash, error) { + panic("implement me") +} + +// GetOrCreateClock return a Lamport clock stored in the Repo. +// If the clock doesn't exist, it's created. +func (repo *GoGitRepo) GetOrCreateClock(name string) (lamport.Clock, error) { + c, err := repo.getClock(name) + if err == nil { + return c, nil + } + if err != ErrClockNotExist { + return nil, err + } + + repo.clocksMutex.Lock() + defer repo.clocksMutex.Unlock() + + p := clockPath + name + "-clock" + + c, err = lamport.NewPersistedClock(p) + if err != nil { + return nil, err + } + + repo.clocks[name] = c + return c, nil +} + +func (repo *GoGitRepo) getClock(name string) (lamport.Clock, error) { + repo.clocksMutex.Lock() + defer repo.clocksMutex.Unlock() + + if c, ok := repo.clocks[name]; ok { + return c, nil + } + + p := clockPath + name + "-clock" + + c, err := lamport.LoadPersistedClock(p) + if err == nil { + repo.clocks[name] = c + return c, nil + } + if err == lamport.ErrClockNotExist { + return nil, ErrClockNotExist + } + return nil, err +} + +// AddRemote add a new remote to the repository +// Not in the interface because it's only used for testing +func (repo *GoGitRepo) AddRemote(name string, url string) error { + _, err := repo.r.CreateRemote(&config.RemoteConfig{ + Name: name, + URLs: []string{url}, + }) + + return err +} diff --git a/repository/gogit_config.go b/repository/gogit_config.go new file mode 100644 index 00000000..0f91b092 --- /dev/null +++ b/repository/gogit_config.go @@ -0,0 +1,175 @@ +package repository + +import ( + "fmt" + "strconv" + "strings" + "time" + + gogit "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing/format/config" +) + +var _ Config = &goGitConfig{} + +type goGitConfig struct { + repo *gogit.Repository +} + +func newGoGitConfig(repo *gogit.Repository) *goGitConfig { + return &goGitConfig{repo: repo} +} + +func (ggc *goGitConfig) StoreString(key, value string) error { + cfg, err := ggc.repo.Config() + if err != nil { + return err + } + + split := strings.Split(key, ".") + + switch { + case len(split) <= 1: + return fmt.Errorf("invalid key") + case len(split) == 2: + cfg.Raw.Section(split[0]).SetOption(split[1], value) + default: + section := split[0] + subsection := strings.Join(split[1:len(split)-2], ".") + option := split[len(split)-1] + cfg.Raw.Section(section).Subsection(subsection).SetOption(option, value) + } + + return ggc.repo.SetConfig(cfg) +} + +func (ggc *goGitConfig) StoreTimestamp(key string, value time.Time) error { + return ggc.StoreString(key, strconv.Itoa(int(value.Unix()))) +} + +func (ggc *goGitConfig) StoreBool(key string, value bool) error { + return ggc.StoreString(key, strconv.FormatBool(value)) +} + +func (ggc *goGitConfig) ReadAll(keyPrefix string) (map[string]string, error) { + cfg, err := ggc.repo.Config() + if err != nil { + return nil, err + } + + split := strings.Split(keyPrefix, ".") + + var opts config.Options + + switch { + case len(split) < 1: + return nil, fmt.Errorf("invalid key prefix") + case len(split) == 1: + opts = cfg.Raw.Section(split[0]).Options + default: + section := split[0] + subsection := strings.Join(split[1:len(split)-1], ".") + opts = cfg.Raw.Section(section).Subsection(subsection).Options + } + + if len(opts) == 0 { + return nil, fmt.Errorf("invalid section") + } + + if keyPrefix[len(keyPrefix)-1:] != "." { + keyPrefix += "." + } + + result := make(map[string]string, len(opts)) + for _, opt := range opts { + result[keyPrefix+opt.Key] = opt.Value + } + + return result, nil +} + +func (ggc *goGitConfig) ReadBool(key string) (bool, error) { + val, err := ggc.ReadString(key) + if err != nil { + return false, err + } + + return strconv.ParseBool(val) +} + +func (ggc *goGitConfig) ReadString(key string) (string, error) { + cfg, err := ggc.repo.Config() + if err != nil { + return "", err + } + + split := strings.Split(key, ".") + + // TODO: return ErrNoConfigEntry and ErrMultipleConfigEntry + // Can use forked go-git: https://github.com/go-git/go-git/pull/112 + + switch { + case len(split) <= 1: + return "", fmt.Errorf("invalid key") + case len(split) == 2: + return cfg.Raw.Section(split[0]).Option(split[1]), nil + default: + section := split[0] + subsection := strings.Join(split[1:len(split)-2], ".") + option := split[len(split)-1] + return cfg.Raw.Section(section).Subsection(subsection).Option(option), nil + } +} + +func (ggc *goGitConfig) ReadTimestamp(key string) (time.Time, error) { + value, err := ggc.ReadString(key) + if err != nil { + return time.Time{}, err + } + return ParseTimestamp(value) +} + +func (ggc *goGitConfig) RemoveAll(keyPrefix string) error { + cfg, err := ggc.repo.Config() + if err != nil { + return err + } + + split := strings.Split(keyPrefix, ".") + + // missing in go-git + hasOption := func(options config.Options, key string) bool { + for _, option := range options { + if option.IsKey(key) { + return true + } + } + return false + } + + switch { + case len(split) < 1: + return fmt.Errorf("invalid key prefix") + case len(split) == 1: + if len(cfg.Raw.Section(split[0]).Options) > 0 { + cfg.Raw.RemoveSection(split[0]) + } else { + return fmt.Errorf("invalid key prefix") + } + default: + section := split[0] + rest := strings.Join(split[1:], ".") + + if cfg.Raw.Section(section).HasSubsection(rest) { + cfg.Raw.RemoveSubsection(section, rest) + } else { + if hasOption(cfg.Raw.Section(section).Options, rest) { + cfg.Raw.Section(section).RemoveOption(rest) + } else { + return fmt.Errorf("invalid key prefix") + } + } + } + + return ggc.repo.SetConfig(cfg) +} diff --git a/repository/gogit_test.go b/repository/gogit_test.go new file mode 100644 index 00000000..9dcf109f --- /dev/null +++ b/repository/gogit_test.go @@ -0,0 +1,68 @@ +package repository + +import ( + "io/ioutil" + "os" + "path" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewGoGitRepo(t *testing.T) { + // Plain + plainRoot, err := ioutil.TempDir("", "") + require.NoError(t, err) + defer os.RemoveAll(plainRoot) + + _, err = InitGoGitRepo(plainRoot) + require.NoError(t, err) + plainGitDir := path.Join(plainRoot, ".git") + + // Bare + bareRoot, err := ioutil.TempDir("", "") + require.NoError(t, err) + defer os.RemoveAll(bareRoot) + + _, err = InitBareGoGitRepo(bareRoot) + require.NoError(t, err) + bareGitDir := bareRoot + + tests := []struct { + inPath string + outPath string + err bool + }{ + // errors + {"/", "", true}, + // parent dir of a repo + {filepath.Dir(plainRoot), "", true}, + + // Plain repo + {plainRoot, plainGitDir, false}, + {plainGitDir, plainGitDir, false}, + {path.Join(plainGitDir, "objects"), plainGitDir, false}, + + // Bare repo + {bareRoot, bareGitDir, false}, + {bareGitDir, bareGitDir, false}, + {path.Join(bareGitDir, "objects"), bareGitDir, false}, + } + + for i, tc := range tests { + r, err := NewGoGitRepo(tc.inPath, nil) + + if tc.err { + require.Error(t, err, i) + } else { + require.NoError(t, err, i) + assert.Equal(t, tc.outPath, r.GetPath(), i) + } + } +} + +func TestGoGitRepo(t *testing.T) { + RepoTest(t, CreateGoGitTestRepo, CleanupTestRepos) +} diff --git a/repository/gogit_testing.go b/repository/gogit_testing.go new file mode 100644 index 00000000..f20ff6be --- /dev/null +++ b/repository/gogit_testing.go @@ -0,0 +1,58 @@ +package repository + +import ( + "io/ioutil" + "log" +) + +// This is intended for testing only + +func CreateGoGitTestRepo(bare bool) TestedRepo { + dir, err := ioutil.TempDir("", "") + if err != nil { + log.Fatal(err) + } + + var creator func(string) (*GoGitRepo, error) + + if bare { + creator = InitBareGoGitRepo + } else { + creator = InitGoGitRepo + } + + repo, err := creator(dir) + if err != nil { + log.Fatal(err) + } + + config := repo.LocalConfig() + if err := config.StoreString("user.name", "testuser"); err != nil { + log.Fatal("failed to set user.name for test repository: ", err) + } + if err := config.StoreString("user.email", "testuser@example.com"); err != nil { + log.Fatal("failed to set user.email for test repository: ", err) + } + + return repo +} + +func SetupGoGitReposAndRemote() (repoA, repoB, remote TestedRepo) { + repoA = CreateGoGitTestRepo(false) + repoB = CreateGoGitTestRepo(false) + remote = CreateGoGitTestRepo(true) + + remoteAddr := "file://" + remote.GetPath() + + err := repoA.AddRemote("origin", remoteAddr) + if err != nil { + log.Fatal(err) + } + + err = repoB.AddRemote("origin", remoteAddr) + if err != nil { + log.Fatal(err) + } + + return repoA, repoB, remote +} From b127481364176ac7ecb56c1604e1460251574859 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Mon, 27 Jul 2020 00:14:01 +0200 Subject: [PATCH 02/25] repository: add access to the system keyring, with fallback on a file --- go.mod | 1 + go.sum | 17 ++++++++++ repository/config.go | 6 ++++ repository/git.go | 16 +++++++-- repository/gogit.go | 18 ++++++++-- repository/keyring.go | 73 +++++++++++++++++++++++++++++++++++++++++ repository/mock_repo.go | 9 +++++ repository/repo.go | 11 +++++-- 8 files changed, 144 insertions(+), 7 deletions(-) create mode 100644 repository/keyring.go diff --git a/go.mod b/go.mod index 9f8b259b..77b3657f 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.12 require ( github.com/99designs/gqlgen v0.10.3-0.20200209012558-b7a58a1c0e4b + github.com/99designs/keyring v1.1.5 github.com/MichaelMure/go-term-text v0.2.9 github.com/araddon/dateparse v0.0.0-20190622164848-0fb0a474d195 github.com/awesome-gocui/gocui v0.6.1-0.20191115151952-a34ffb055986 diff --git a/go.sum b/go.sum index 9b84ea6d..f0531a1c 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/99designs/gqlgen v0.10.3-0.20200209012558-b7a58a1c0e4b h1:510xa84qGbDemwTHNio4cLWkdKFxxJgVtsIOH+Ku8bo= github.com/99designs/gqlgen v0.10.3-0.20200209012558-b7a58a1c0e4b/go.mod h1:dfBhwZKMcSYiYRMTs8qWF+Oha6782e1xPfgRmVal9I8= +github.com/99designs/keyring v1.1.5 h1:wLv7QyzYpFIyMSwOADq1CLTF9KbjbBfcnfmOGJ64aO4= +github.com/99designs/keyring v1.1.5/go.mod h1:7hsVvt2qXgtadGevGJ4ujg+u8m6SpJ5TpHqTozIPqf0= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Masterminds/glide v0.13.2/go.mod h1:STyF5vcenH/rUqTEv+/hBXlSTo7KYwg2oc2f4tzPWic= github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= @@ -53,6 +55,8 @@ github.com/corpix/uarand v0.1.1/go.mod h1:SFKZvkcRoLqVRFZ4u25xPmp6m9ktANfbpXZ7SJ github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/danieljoos/wincred v1.0.2 h1:zf4bhty2iLuwgjgpraD2E9UbvO+fe54XXGJbOwe23fU= +github.com/danieljoos/wincred v1.0.2/go.mod h1:SnuYRW9lp1oJrZX/dXJqr0cPK5gYXqx3EJbmjhLdK9U= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -62,6 +66,8 @@ github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4 github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= +github.com/dvsekhvalnov/jose2go v0.0.0-20180829124132-7f401d37b68a h1:mq+R6XEM6lJX5VlLyZIrUSP8tSuJp82xTK89hvBwJbU= +github.com/dvsekhvalnov/jose2go v0.0.0-20180829124132-7f401d37b68a/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM= github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= @@ -85,6 +91,8 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2 github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= +github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= @@ -112,6 +120,8 @@ github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoA github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= @@ -136,6 +146,8 @@ github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY= github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/keybase/go-keychain v0.0.0-20190712205309-48d3d31d256d h1:Z+RDyXzjKE0i2sTjZ/b1uxiGtPhFy34Ou/Tk0qwN0kM= +github.com/keybase/go-keychain v0.0.0-20190712205309-48d3d31d256d/go.mod h1:JJNrCn9otv/2QP4D7SMJBgaleKpOf66PnW6F5WGNRIc= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -167,6 +179,8 @@ github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= +github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/ngdinhtoan/glide-cleanup v0.2.0/go.mod h1:UQzsmiDOb8YV3nOsCxK/c9zPpCZVNoHScRE3EO9pVMM= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= @@ -232,6 +246,7 @@ github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -263,6 +278,7 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM= @@ -302,6 +318,7 @@ golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpbl golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/repository/config.go b/repository/config.go index 4fa5c69b..2133b169 100644 --- a/repository/config.go +++ b/repository/config.go @@ -1,10 +1,16 @@ package repository import ( + "errors" "strconv" "time" ) +var ( + ErrNoConfigEntry = errors.New("no config entry for the given key") + ErrMultipleConfigEntry = errors.New("multiple config entry for the given key") +) + // Config represent the common function interacting with the repository config storage type Config interface { // Store writes a single key/value pair in the config diff --git a/repository/git.go b/repository/git.go index 3d756324..85107ba5 100644 --- a/repository/git.go +++ b/repository/git.go @@ -26,6 +26,8 @@ type GitRepo struct { clocksMutex sync.Mutex clocks map[string]lamport.Clock + + keyring Keyring } // LocalConfig give access to the repository scoped configuration @@ -38,6 +40,10 @@ func (repo *GitRepo) GlobalConfig() Config { return newGitConfig(repo, true) } +func (repo *GitRepo) Keyring() Keyring { + return repo.keyring +} + // Run the given git command with the given I/O reader/writers, returning an error if it fails. func (repo *GitRepo) runGitCommandWithIO(stdin io.Reader, stdout, stderr io.Writer, args ...string) error { // make sure that the working directory for the command @@ -83,9 +89,15 @@ func (repo *GitRepo) runGitCommand(args ...string) (string, error) { // NewGitRepo determines if the given working directory is inside of a git repository, // and returns the corresponding GitRepo instance if it is. func NewGitRepo(path string, clockLoaders []ClockLoader) (*GitRepo, error) { + k, err := defaultKeyring() + if err != nil { + return nil, err + } + repo := &GitRepo{ - path: path, - clocks: make(map[string]lamport.Clock), + path: path, + clocks: make(map[string]lamport.Clock), + keyring: k, } // Check the repo and retrieve the root path diff --git a/repository/gogit.go b/repository/gogit.go index 71a7e6d0..f115fab5 100644 --- a/repository/gogit.go +++ b/repository/gogit.go @@ -24,6 +24,8 @@ type GoGitRepo struct { clocksMutex sync.Mutex clocks map[string]lamport.Clock + + keyring Keyring } func NewGoGitRepo(path string, clockLoaders []ClockLoader) (*GoGitRepo, error) { @@ -37,10 +39,16 @@ func NewGoGitRepo(path string, clockLoaders []ClockLoader) (*GoGitRepo, error) { return nil, err } + k, err := defaultKeyring() + if err != nil { + return nil, err + } + repo := &GoGitRepo{ - r: r, - path: path, - clocks: make(map[string]lamport.Clock), + r: r, + path: path, + clocks: make(map[string]lamport.Clock), + keyring: k, } for _, loader := range clockLoaders { @@ -154,6 +162,10 @@ func (repo *GoGitRepo) GlobalConfig() Config { panic("go-git doesn't support writing global config") } +func (repo *GoGitRepo) Keyring() Keyring { + return repo.keyring +} + // GetPath returns the path to the repo. func (repo *GoGitRepo) GetPath() string { return repo.path diff --git a/repository/keyring.go b/repository/keyring.go new file mode 100644 index 00000000..9f8171db --- /dev/null +++ b/repository/keyring.go @@ -0,0 +1,73 @@ +package repository + +import ( + "os" + "path" + + "github.com/99designs/keyring" +) + +type Item = keyring.Item + +var ErrKeyringKeyNotFound = keyring.ErrKeyNotFound + +// Keyring provides the uniform interface over the underlying backends +type Keyring interface { + // Returns an Item matching the key or ErrKeyringKeyNotFound + Get(key string) (Item, error) + // Stores an Item on the keyring + Set(item Item) error + // Removes the item with matching key + Remove(key string) error + // Provides a slice of all keys stored on the keyring + Keys() ([]string, error) +} + +func defaultKeyring() (Keyring, error) { + ucd, err := os.UserConfigDir() + if err != nil { + return nil, err + } + + backends := []keyring.BackendType{ + keyring.WinCredBackend, + keyring.KeychainBackend, + keyring.PassBackend, + keyring.FileBackend, + } + + return keyring.Open(keyring.Config{ + // TODO: ideally this would not be there, it disable the freedesktop backend on linux + // due to https://github.com/99designs/keyring/issues/44 + AllowedBackends: backends, + + ServiceName: "git-bug", + + // MacOS keychain + KeychainName: "git-bug", + KeychainTrustApplication: true, + + // KDE Wallet + KWalletAppID: "git-bug", + KWalletFolder: "git-bug", + + // Windows + WinCredPrefix: "git-bug", + + // freedesktop.org's Secret Service + LibSecretCollectionName: "git-bug", + + // Pass (https://www.passwordstore.org/) + PassPrefix: "git-bug", + + // Fallback encrypted file + FileDir: path.Join(ucd, "git-bug", "keyring"), + // As we write the file in the user's config directory, this file should already be protected by the OS against + // other user's access. We actually don't terribly need to protect it further and a password prompt across all + // UI's would be a pain. Therefore we use here a constant password so the file will be unreadable by generic file + // scanners if the user's machine get compromised. + FilePasswordFunc: func(string) (string, error) { + return "git-bug", nil + }, + }) +} diff --git a/repository/mock_repo.go b/repository/mock_repo.go index 576e984e..b3b4cb41 100644 --- a/repository/mock_repo.go +++ b/repository/mock_repo.go @@ -5,6 +5,8 @@ import ( "fmt" "strings" + "github.com/99designs/keyring" + "github.com/MichaelMure/git-bug/util/lamport" ) @@ -15,6 +17,7 @@ var _ TestedRepo = &mockRepoForTest{} type mockRepoForTest struct { config *MemConfig globalConfig *MemConfig + keyring *keyring.ArrayKeyring blobs map[Hash][]byte trees map[Hash]string commits map[Hash]commit @@ -31,6 +34,7 @@ func NewMockRepoForTest() *mockRepoForTest { return &mockRepoForTest{ config: NewMemConfig(), globalConfig: NewMemConfig(), + keyring: keyring.NewArrayKeyring(nil), blobs: make(map[Hash][]byte), trees: make(map[Hash]string), commits: make(map[Hash]commit), @@ -49,6 +53,11 @@ func (r *mockRepoForTest) GlobalConfig() Config { return r.globalConfig } +// Keyring give access to a user-wide storage for secrets +func (r *mockRepoForTest) Keyring() Keyring { + return r.keyring +} + // GetPath returns the path to the repo. func (r *mockRepoForTest) GetPath() string { return "~/mockRepo/" diff --git a/repository/repo.go b/repository/repo.go index 30d95806..696b032e 100644 --- a/repository/repo.go +++ b/repository/repo.go @@ -10,8 +10,6 @@ import ( ) var ( - ErrNoConfigEntry = errors.New("no config entry for the given key") - ErrMultipleConfigEntry = errors.New("multiple config entry for the given key") // ErrNotARepo is the error returned when the git repo root wan't be found ErrNotARepo = errors.New("not a git repository") // ErrClockNotExist is the error returned when a clock can't be found @@ -24,9 +22,17 @@ type RepoConfig interface { LocalConfig() Config // GlobalConfig give access to the git global configuration + // Deprecated: to remove in favor of Keyring() + // TODO: remove GlobalConfig() Config } +// RepoKeyring give access to a user-wide storage for secrets +type RepoKeyring interface { + // Keyring give access to a user-wide storage for secrets + Keyring() Keyring +} + // RepoCommon represent the common function the we want all the repo to implement type RepoCommon interface { // GetPath returns the path to the repo. @@ -48,6 +54,7 @@ type RepoCommon interface { // Repo represents a source code repository. type Repo interface { RepoConfig + RepoKeyring RepoCommon // FetchRefs fetch git refs from a remote From 3ecbf8db28105d147340b80d65a5a6d537233135 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Tue, 28 Jul 2020 12:56:46 +0200 Subject: [PATCH 03/25] bridge: store credentials in the Keyring instead of the git config --- bridge/core/auth/credential.go | 212 +++++++++++----------------- bridge/core/auth/credential_base.go | 44 +++++- bridge/core/auth/login.go | 8 +- bridge/core/auth/login_password.go | 14 +- bridge/core/auth/options.go | 22 +-- bridge/core/auth/token.go | 8 +- bridge/github/config.go | 2 +- bridge/gitlab/config.go | 2 +- bridge/jira/config.go | 2 +- cache/repo_cache_common.go | 5 +- repository/git_testing.go | 18 ++- repository/repo.go | 5 - 12 files changed, 166 insertions(+), 176 deletions(-) diff --git a/bridge/core/auth/credential.go b/bridge/core/auth/credential.go index d95b23c7..2327a6fc 100644 --- a/bridge/core/auth/credential.go +++ b/bridge/core/auth/credential.go @@ -1,11 +1,11 @@ package auth import ( - "crypto/rand" "encoding/base64" + "encoding/json" "errors" "fmt" - "regexp" + "strconv" "strings" "time" @@ -14,12 +14,12 @@ import ( ) const ( - configKeyPrefix = "git-bug.auth" - configKeyKind = "kind" - configKeyTarget = "target" - configKeyCreateTime = "createtime" - configKeySalt = "salt" - configKeyPrefixMeta = "meta." + keyringKeyPrefix = "auth-" + keyringKeyKind = "kind" + keyringKeyTarget = "target" + keyringKeyCreateTime = "createtime" + keyringKeySalt = "salt" + keyringKeyPrefixMeta = "meta." MetaKeyLogin = "login" MetaKeyBaseURL = "base-url" @@ -57,22 +57,23 @@ type Credential interface { } // Load loads a credential from the repo config -func LoadWithId(repo repository.RepoConfig, id entity.Id) (Credential, error) { - keyPrefix := fmt.Sprintf("%s.%s.", configKeyPrefix, id) +func LoadWithId(repo repository.RepoKeyring, id entity.Id) (Credential, error) { + key := fmt.Sprintf("%s%s", keyringKeyPrefix, id) - // read token config pairs - rawconfigs, err := repo.GlobalConfig().ReadAll(keyPrefix) - if err != nil { - // Not exactly right due to the limitation of ReadAll() + item, err := repo.Keyring().Get(key) + if err == repository.ErrKeyringKeyNotFound { return nil, ErrCredentialNotExist } + if err != nil { + return nil, err + } - return loadFromConfig(rawconfigs, id) + return decode(item) } // LoadWithPrefix load a credential from the repo config with a prefix -func LoadWithPrefix(repo repository.RepoConfig, prefix string) (Credential, error) { - creds, err := List(repo) +func LoadWithPrefix(repo repository.RepoKeyring, prefix string) (Credential, error) { + keys, err := repo.Keyring().Keys() if err != nil { return nil, err } @@ -80,10 +81,22 @@ func LoadWithPrefix(repo repository.RepoConfig, prefix string) (Credential, erro // preallocate but empty matching := make([]Credential, 0, 5) - for _, cred := range creds { - if cred.ID().HasPrefix(prefix) { - matching = append(matching, cred) + for _, key := range keys { + if !strings.HasPrefix(key, keyringKeyPrefix+prefix) { + continue } + + item, err := repo.Keyring().Get(key) + if err != nil { + return nil, err + } + + cred, err := decode(item) + if err != nil { + return nil, err + } + + matching = append(matching, cred) } if len(matching) > 1 { @@ -101,29 +114,25 @@ func LoadWithPrefix(repo repository.RepoConfig, prefix string) (Credential, erro return matching[0], nil } -// loadFromConfig is a helper to construct a Credential from the set of git configs -func loadFromConfig(rawConfigs map[string]string, id entity.Id) (Credential, error) { - keyPrefix := fmt.Sprintf("%s.%s.", configKeyPrefix, id) +// decode is a helper to construct a Credential from the keyring Item +func decode(item repository.Item) (Credential, error) { + data := make(map[string]string) - // trim key prefix - configs := make(map[string]string) - for key, value := range rawConfigs { - newKey := strings.TrimPrefix(key, keyPrefix) - configs[newKey] = value + err := json.Unmarshal(item.Data, &data) + if err != nil { + return nil, err } var cred Credential - var err error - - switch CredentialKind(configs[configKeyKind]) { + switch CredentialKind(data[keyringKeyKind]) { case KindToken: - cred, err = NewTokenFromConfig(configs) + cred, err = NewTokenFromConfig(data) case KindLogin: - cred, err = NewLoginFromConfig(configs) + cred, err = NewLoginFromConfig(data) case KindLoginPassword: - cred, err = NewLoginPasswordFromConfig(configs) + cred, err = NewLoginPasswordFromConfig(data) default: - return nil, fmt.Errorf("unknown credential type \"%s\"", configs[configKeyKind]) + return nil, fmt.Errorf("unknown credential type \"%s\"", data[keyringKeyKind]) } if err != nil { @@ -133,64 +142,27 @@ func loadFromConfig(rawConfigs map[string]string, id entity.Id) (Credential, err return cred, nil } -func metaFromConfig(configs map[string]string) map[string]string { - result := make(map[string]string) - for key, val := range configs { - if strings.HasPrefix(key, configKeyPrefixMeta) { - key = strings.TrimPrefix(key, configKeyPrefixMeta) - result[key] = val - } - } - if len(result) == 0 { - return nil - } - return result -} - -func makeSalt() []byte { - result := make([]byte, 16) - _, err := rand.Read(result) - if err != nil { - panic(err) - } - return result -} - -func saltFromConfig(configs map[string]string) ([]byte, error) { - val, ok := configs[configKeySalt] - if !ok { - return nil, fmt.Errorf("no credential salt found") - } - return base64.StdEncoding.DecodeString(val) -} - // List load all existing credentials -func List(repo repository.RepoConfig, opts ...Option) ([]Credential, error) { - rawConfigs, err := repo.GlobalConfig().ReadAll(configKeyPrefix + ".") +func List(repo repository.RepoKeyring, opts ...ListOption) ([]Credential, error) { + keys, err := repo.Keyring().Keys() if err != nil { return nil, err } - re := regexp.MustCompile(`^` + configKeyPrefix + `\.([^.]+)\.([^.]+(?:\.[^.]+)*)$`) - - mapped := make(map[string]map[string]string) - - for key, val := range rawConfigs { - res := re.FindStringSubmatch(key) - if res == nil { - continue - } - if mapped[res[1]] == nil { - mapped[res[1]] = make(map[string]string) - } - mapped[res[1]][res[2]] = val - } - matcher := matcher(opts) var credentials []Credential - for id, kvs := range mapped { - cred, err := loadFromConfig(kvs, entity.Id(id)) + for _, key := range keys { + if !strings.HasPrefix(key, keyringKeyPrefix) { + continue + } + + item, err := repo.Keyring().Get(key) + if err != nil { + return nil, err + } + + cred, err := decode(item) if err != nil { return nil, err } @@ -203,74 +175,48 @@ func List(repo repository.RepoConfig, opts ...Option) ([]Credential, error) { } // IdExist return whether a credential id exist or not -func IdExist(repo repository.RepoConfig, id entity.Id) bool { +func IdExist(repo repository.RepoKeyring, id entity.Id) bool { _, err := LoadWithId(repo, id) return err == nil } // PrefixExist return whether a credential id prefix exist or not -func PrefixExist(repo repository.RepoConfig, prefix string) bool { +func PrefixExist(repo repository.RepoKeyring, prefix string) bool { _, err := LoadWithPrefix(repo, prefix) return err == nil } // Store stores a credential in the global git config -func Store(repo repository.RepoConfig, cred Credential) error { - confs := cred.toConfig() - - prefix := fmt.Sprintf("%s.%s.", configKeyPrefix, cred.ID()) - - // Kind - err := repo.GlobalConfig().StoreString(prefix+configKeyKind, string(cred.Kind())) - if err != nil { - return err - } - - // Target - err = repo.GlobalConfig().StoreString(prefix+configKeyTarget, cred.Target()) - if err != nil { - return err - } - - // CreateTime - err = repo.GlobalConfig().StoreTimestamp(prefix+configKeyCreateTime, cred.CreateTime()) - if err != nil { - return err - } - - // Salt +func Store(repo repository.RepoKeyring, cred Credential) error { if len(cred.Salt()) != 16 { panic("credentials need to be salted") } - encoded := base64.StdEncoding.EncodeToString(cred.Salt()) - err = repo.GlobalConfig().StoreString(prefix+configKeySalt, encoded) + + confs := cred.toConfig() + + confs[keyringKeyKind] = string(cred.Kind()) + confs[keyringKeyTarget] = cred.Target() + confs[keyringKeyCreateTime] = strconv.Itoa(int(cred.CreateTime().Unix())) + confs[keyringKeySalt] = base64.StdEncoding.EncodeToString(cred.Salt()) + + for key, val := range cred.Metadata() { + confs[keyringKeyPrefixMeta+key] = val + } + + data, err := json.Marshal(confs) if err != nil { return err } - // Metadata - for key, val := range cred.Metadata() { - err := repo.GlobalConfig().StoreString(prefix+configKeyPrefixMeta+key, val) - if err != nil { - return err - } - } - - // Custom - for key, val := range confs { - err := repo.GlobalConfig().StoreString(prefix+key, val) - if err != nil { - return err - } - } - - return nil + return repo.Keyring().Set(repository.Item{ + Key: keyringKeyPrefix + cred.ID().String(), + Data: data, + }) } // Remove removes a credential from the global git config -func Remove(repo repository.RepoConfig, id entity.Id) error { - keyPrefix := fmt.Sprintf("%s.%s", configKeyPrefix, id) - return repo.GlobalConfig().RemoveAll(keyPrefix) +func Remove(repo repository.RepoKeyring, id entity.Id) error { + return repo.Keyring().Remove(keyringKeyPrefix + id.String()) } /* diff --git a/bridge/core/auth/credential_base.go b/bridge/core/auth/credential_base.go index 488c223c..f9d1bf67 100644 --- a/bridge/core/auth/credential_base.go +++ b/bridge/core/auth/credential_base.go @@ -1,7 +1,10 @@ package auth import ( + "crypto/rand" + "encoding/base64" "fmt" + "strings" "time" "github.com/MichaelMure/git-bug/bridge/core" @@ -23,13 +26,22 @@ func newCredentialBase(target string) *credentialBase { } } -func newCredentialBaseFromConfig(conf map[string]string) (*credentialBase, error) { +func makeSalt() []byte { + result := make([]byte, 16) + _, err := rand.Read(result) + if err != nil { + panic(err) + } + return result +} + +func newCredentialBaseFromData(data map[string]string) (*credentialBase, error) { base := &credentialBase{ - target: conf[configKeyTarget], - meta: metaFromConfig(conf), + target: data[keyringKeyTarget], + meta: metaFromData(data), } - if createTime, ok := conf[configKeyCreateTime]; ok { + if createTime, ok := data[keyringKeyCreateTime]; ok { t, err := repository.ParseTimestamp(createTime) if err != nil { return nil, err @@ -39,7 +51,7 @@ func newCredentialBaseFromConfig(conf map[string]string) (*credentialBase, error return nil, fmt.Errorf("missing create time") } - salt, err := saltFromConfig(conf) + salt, err := saltFromData(data) if err != nil { return nil, err } @@ -48,6 +60,28 @@ func newCredentialBaseFromConfig(conf map[string]string) (*credentialBase, error return base, nil } +func metaFromData(data map[string]string) map[string]string { + result := make(map[string]string) + for key, val := range data { + if strings.HasPrefix(key, keyringKeyPrefixMeta) { + key = strings.TrimPrefix(key, keyringKeyPrefixMeta) + result[key] = val + } + } + if len(result) == 0 { + return nil + } + return result +} + +func saltFromData(data map[string]string) ([]byte, error) { + val, ok := data[keyringKeySalt] + if !ok { + return nil, fmt.Errorf("no credential salt found") + } + return base64.StdEncoding.DecodeString(val) +} + func (cb *credentialBase) Target() string { return cb.target } diff --git a/bridge/core/auth/login.go b/bridge/core/auth/login.go index ea74835a..496f2412 100644 --- a/bridge/core/auth/login.go +++ b/bridge/core/auth/login.go @@ -8,7 +8,7 @@ import ( ) const ( - configKeyLoginLogin = "login" + keyringKeyLoginLogin = "login" ) var _ Credential = &Login{} @@ -26,14 +26,14 @@ func NewLogin(target, login string) *Login { } func NewLoginFromConfig(conf map[string]string) (*Login, error) { - base, err := newCredentialBaseFromConfig(conf) + base, err := newCredentialBaseFromData(conf) if err != nil { return nil, err } return &Login{ credentialBase: base, - Login: conf[configKeyLoginLogin], + Login: conf[keyringKeyLoginLogin], }, nil } @@ -62,6 +62,6 @@ func (lp *Login) Validate() error { func (lp *Login) toConfig() map[string]string { return map[string]string{ - configKeyLoginLogin: lp.Login, + keyringKeyLoginLogin: lp.Login, } } diff --git a/bridge/core/auth/login_password.go b/bridge/core/auth/login_password.go index 1981026a..166e37fb 100644 --- a/bridge/core/auth/login_password.go +++ b/bridge/core/auth/login_password.go @@ -8,8 +8,8 @@ import ( ) const ( - configKeyLoginPasswordLogin = "login" - configKeyLoginPasswordPassword = "password" + keyringKeyLoginPasswordLogin = "login" + keyringKeyLoginPasswordPassword = "password" ) var _ Credential = &LoginPassword{} @@ -29,15 +29,15 @@ func NewLoginPassword(target, login, password string) *LoginPassword { } func NewLoginPasswordFromConfig(conf map[string]string) (*LoginPassword, error) { - base, err := newCredentialBaseFromConfig(conf) + base, err := newCredentialBaseFromData(conf) if err != nil { return nil, err } return &LoginPassword{ credentialBase: base, - Login: conf[configKeyLoginPasswordLogin], - Password: conf[configKeyLoginPasswordPassword], + Login: conf[keyringKeyLoginPasswordLogin], + Password: conf[keyringKeyLoginPasswordPassword], }, nil } @@ -70,7 +70,7 @@ func (lp *LoginPassword) Validate() error { func (lp *LoginPassword) toConfig() map[string]string { return map[string]string{ - configKeyLoginPasswordLogin: lp.Login, - configKeyLoginPasswordPassword: lp.Password, + keyringKeyLoginPasswordLogin: lp.Login, + keyringKeyLoginPasswordPassword: lp.Password, } } diff --git a/bridge/core/auth/options.go b/bridge/core/auth/options.go index 1d8c44d1..00c6e3ec 100644 --- a/bridge/core/auth/options.go +++ b/bridge/core/auth/options.go @@ -1,22 +1,22 @@ package auth -type options struct { +type listOptions struct { target string kind map[CredentialKind]interface{} meta map[string]string } -type Option func(opts *options) +type ListOption func(opts *listOptions) -func matcher(opts []Option) *options { - result := &options{} +func matcher(opts []ListOption) *listOptions { + result := &listOptions{} for _, opt := range opts { opt(result) } return result } -func (opts *options) Match(cred Credential) bool { +func (opts *listOptions) Match(cred Credential) bool { if opts.target != "" && cred.Target() != opts.target { return false } @@ -35,15 +35,15 @@ func (opts *options) Match(cred Credential) bool { return true } -func WithTarget(target string) Option { - return func(opts *options) { +func WithTarget(target string) ListOption { + return func(opts *listOptions) { opts.target = target } } // WithKind match credentials with the given kind. Can be specified multiple times. -func WithKind(kind CredentialKind) Option { - return func(opts *options) { +func WithKind(kind CredentialKind) ListOption { + return func(opts *listOptions) { if opts.kind == nil { opts.kind = make(map[CredentialKind]interface{}) } @@ -51,8 +51,8 @@ func WithKind(kind CredentialKind) Option { } } -func WithMeta(key string, val string) Option { - return func(opts *options) { +func WithMeta(key string, val string) ListOption { + return func(opts *listOptions) { if opts.meta == nil { opts.meta = make(map[string]string) } diff --git a/bridge/core/auth/token.go b/bridge/core/auth/token.go index 1f019f44..84d6ac13 100644 --- a/bridge/core/auth/token.go +++ b/bridge/core/auth/token.go @@ -8,7 +8,7 @@ import ( ) const ( - configKeyTokenValue = "value" + keyringKeyTokenValue = "value" ) var _ Credential = &Token{} @@ -28,14 +28,14 @@ func NewToken(target, value string) *Token { } func NewTokenFromConfig(conf map[string]string) (*Token, error) { - base, err := newCredentialBaseFromConfig(conf) + base, err := newCredentialBaseFromData(conf) if err != nil { return nil, err } return &Token{ credentialBase: base, - Value: conf[configKeyTokenValue], + Value: conf[keyringKeyTokenValue], }, nil } @@ -65,6 +65,6 @@ func (t *Token) Validate() error { func (t *Token) toConfig() map[string]string { return map[string]string{ - configKeyTokenValue: t.Value, + keyringKeyTokenValue: t.Value, } } diff --git a/bridge/github/config.go b/bridge/github/config.go index 61d641a6..130b0ad1 100644 --- a/bridge/github/config.go +++ b/bridge/github/config.go @@ -239,7 +239,7 @@ func randomFingerprint() string { return string(b) } -func promptTokenOptions(repo repository.RepoConfig, login, owner, project string) (auth.Credential, error) { +func promptTokenOptions(repo repository.RepoKeyring, login, owner, project string) (auth.Credential, error) { creds, err := auth.List(repo, auth.WithTarget(target), auth.WithKind(auth.KindToken), diff --git a/bridge/gitlab/config.go b/bridge/gitlab/config.go index e4e3d8e3..dfac4c54 100644 --- a/bridge/gitlab/config.go +++ b/bridge/gitlab/config.go @@ -156,7 +156,7 @@ func (g *Gitlab) ValidateConfig(conf core.Configuration) error { return nil } -func promptTokenOptions(repo repository.RepoConfig, login, baseUrl string) (auth.Credential, error) { +func promptTokenOptions(repo repository.RepoKeyring, login, baseUrl string) (auth.Credential, error) { creds, err := auth.List(repo, auth.WithTarget(target), auth.WithKind(auth.KindToken), diff --git a/bridge/jira/config.go b/bridge/jira/config.go index ffd3bdc1..717046e2 100644 --- a/bridge/jira/config.go +++ b/bridge/jira/config.go @@ -163,7 +163,7 @@ func (*Jira) ValidateConfig(conf core.Configuration) error { return nil } -func promptCredOptions(repo repository.RepoConfig, login, baseUrl string) (auth.Credential, error) { +func promptCredOptions(repo repository.RepoKeyring, login, baseUrl string) (auth.Credential, error) { creds, err := auth.List(repo, auth.WithTarget(target), auth.WithKind(auth.KindToken), diff --git a/cache/repo_cache_common.go b/cache/repo_cache_common.go index a931f2be..1e330a4a 100644 --- a/cache/repo_cache_common.go +++ b/cache/repo_cache_common.go @@ -20,9 +20,8 @@ func (c *RepoCache) LocalConfig() repository.Config { return c.repo.LocalConfig() } -// GlobalConfig give access to the git global configuration -func (c *RepoCache) GlobalConfig() repository.Config { - return c.repo.GlobalConfig() +func (c *RepoCache) Keyring() repository.Keyring { + return c.repo.Keyring() } // GetPath returns the path to the repo. diff --git a/repository/git_testing.go b/repository/git_testing.go index 5ae4ccc9..7d40bf1f 100644 --- a/repository/git_testing.go +++ b/repository/git_testing.go @@ -3,6 +3,8 @@ package repository import ( "io/ioutil" "log" + + "github.com/99designs/keyring" ) // This is intended for testing only @@ -34,7 +36,11 @@ func CreateTestRepo(bare bool) TestedRepo { log.Fatal("failed to set user.email for test repository: ", err) } - return repo + // make sure we use a mock keyring for testing to not interact with the global system + return &replaceKeyring{ + TestedRepo: repo, + keyring: keyring.NewArrayKeyring(nil), + } } func SetupReposAndRemote() (repoA, repoB, remote TestedRepo) { @@ -56,3 +62,13 @@ func SetupReposAndRemote() (repoA, repoB, remote TestedRepo) { return repoA, repoB, remote } + +// replaceKeyring allow to replace the Keyring of the underlying repo +type replaceKeyring struct { + TestedRepo + keyring Keyring +} + +func (rk replaceKeyring) Keyring() Keyring { + return rk.keyring +} diff --git a/repository/repo.go b/repository/repo.go index 696b032e..7a69a14f 100644 --- a/repository/repo.go +++ b/repository/repo.go @@ -20,11 +20,6 @@ var ( type RepoConfig interface { // LocalConfig give access to the repository scoped configuration LocalConfig() Config - - // GlobalConfig give access to the git global configuration - // Deprecated: to remove in favor of Keyring() - // TODO: remove - GlobalConfig() Config } // RepoKeyring give access to a user-wide storage for secrets From 30d1640bf47fcd14b1d26e8f5965bb61ae61859f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Tue, 28 Jul 2020 13:02:32 +0200 Subject: [PATCH 04/25] repository: some light shuffling of code --- repository/repo.go | 49 ++++++++-------------------------------- repository/tree_entry.go | 32 ++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 39 deletions(-) diff --git a/repository/repo.go b/repository/repo.go index 7a69a14f..806edace 100644 --- a/repository/repo.go +++ b/repository/repo.go @@ -2,9 +2,7 @@ package repository import ( - "bytes" "errors" - "strings" "github.com/MichaelMure/git-bug/util/lamport" ) @@ -16,6 +14,14 @@ var ( ErrClockNotExist = errors.New("clock doesn't exist") ) +// Repo represents a source code repository. +type Repo interface { + RepoConfig + RepoKeyring + RepoCommon + RepoData +} + // RepoConfig access the configuration of a repository type RepoConfig interface { // LocalConfig give access to the repository scoped configuration @@ -46,12 +52,8 @@ type RepoCommon interface { GetRemotes() (map[string]string, error) } -// Repo represents a source code repository. -type Repo interface { - RepoConfig - RepoKeyring - RepoCommon - +// RepoData give access to the git data storage +type RepoData interface { // FetchRefs fetch git refs from a remote FetchRefs(remote string, refSpec string) (string, error) @@ -122,37 +124,6 @@ type ClockLoader struct { Witnesser func(repo ClockedRepo) error } -func prepareTreeEntries(entries []TreeEntry) bytes.Buffer { - var buffer bytes.Buffer - - for _, entry := range entries { - buffer.WriteString(entry.Format()) - } - - return buffer -} - -func readTreeEntries(s string) ([]TreeEntry, error) { - split := strings.Split(strings.TrimSpace(s), "\n") - - casted := make([]TreeEntry, len(split)) - for i, line := range split { - if line == "" { - continue - } - - entry, err := ParseTreeEntry(line) - - if err != nil { - return nil, err - } - - casted[i] = entry - } - - return casted, nil -} - // TestedRepo is an extended ClockedRepo with function for testing only type TestedRepo interface { ClockedRepo diff --git a/repository/tree_entry.go b/repository/tree_entry.go index 8b3de8e2..6c5ec1a5 100644 --- a/repository/tree_entry.go +++ b/repository/tree_entry.go @@ -1,6 +1,7 @@ package repository import ( + "bytes" "fmt" "strings" ) @@ -68,3 +69,34 @@ func ParseObjectType(mode, objType string) (ObjectType, error) { return Unknown, fmt.Errorf("Unknown git object type %s %s", mode, objType) } } + +func prepareTreeEntries(entries []TreeEntry) bytes.Buffer { + var buffer bytes.Buffer + + for _, entry := range entries { + buffer.WriteString(entry.Format()) + } + + return buffer +} + +func readTreeEntries(s string) ([]TreeEntry, error) { + split := strings.Split(strings.TrimSpace(s), "\n") + + casted := make([]TreeEntry, len(split)) + for i, line := range split { + if line == "" { + continue + } + + entry, err := ParseTreeEntry(line) + + if err != nil { + return nil, err + } + + casted[i] = entry + } + + return casted, nil +} From 9c1087e18d2b4f7d5d9f0e98136933d05ce13827 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sun, 30 Aug 2020 11:56:34 +0200 Subject: [PATCH 05/25] repository: fix a todo in the gogit repo --- go.mod | 4 +++ go.sum | 16 ++++++++++-- repository/gogit_config.go | 51 +++++++++++++++++++++++--------------- 3 files changed, 49 insertions(+), 22 deletions(-) diff --git a/go.mod b/go.mod index 77b3657f..cb22412c 100644 --- a/go.mod +++ b/go.mod @@ -32,3 +32,7 @@ require ( golang.org/x/sync v0.0.0-20190423024810-112230192c58 golang.org/x/text v0.3.3 ) + +// Use a forked go-git for now until https://github.com/go-git/go-git/pull/112 is merged +// and released. +replace github.com/go-git/go-git/v5 => github.com/MichaelMure/go-git/v5 v5.1.1-0.20200827115354-b40ca794fe33 diff --git a/go.sum b/go.sum index f0531a1c..afbb0909 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,17 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/Masterminds/glide v0.13.2/go.mod h1:STyF5vcenH/rUqTEv+/hBXlSTo7KYwg2oc2f4tzPWic= github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/vcs v1.13.0/go.mod h1:N09YCmOQr6RLxC6UNHzuVwAdodYbbnycGHSmwVJjcKA= +<<<<<<< HEAD +======= +github.com/MichaelMure/go-git/v5 v5.1.1-0.20200827115354-b40ca794fe33 h1:QFzkZPUMm0HRZ0dZ+GgDKHPUrgUrH3CbcyuzQlhBeww= +github.com/MichaelMure/go-git/v5 v5.1.1-0.20200827115354-b40ca794fe33/go.mod h1:kh02eMX+wdqqxgNMEyq8YgwlIOsDOa9homkUq1PoTMs= +github.com/MichaelMure/go-term-text v0.2.6 h1:dSmJSzk2iI5xWymSMrMbdVM1bxYWu3DjDFhdcJvAuqA= +github.com/MichaelMure/go-term-text v0.2.6/go.mod h1:o2Z5T3b28F4kwAojGvvNdbzjHf9t18vbQ7E2pmTe2Ww= +github.com/MichaelMure/go-term-text v0.2.7 h1:nSYvYGwXxJoiQu6kdGSErpxZ6ah/4WlJyp/niqQor6g= +github.com/MichaelMure/go-term-text v0.2.7/go.mod h1:6z+q5b/nP1V8I9KkWQcUi5QpmF8DVrz9vLJ4hdoxHnM= +github.com/MichaelMure/go-term-text v0.2.8 h1:daXIVPjPkAhcLhA+tfjQBHYjatb1D42/LY1Nw2PXYlU= +github.com/MichaelMure/go-term-text v0.2.8/go.mod h1:6z+q5b/nP1V8I9KkWQcUi5QpmF8DVrz9vLJ4hdoxHnM= +>>>>>>> d2f92f6... repository: fix a todo in the gogit repo github.com/MichaelMure/go-term-text v0.2.9 h1:jUxInT3rDhl4WoJgLnmMS3hR79zigyJS1TqKFDTI6xE= github.com/MichaelMure/go-term-text v0.2.9/go.mod h1:2QSU/Nn2u41Tqoar+90RlYuhjngJPYgod7evnsYwkWc= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= @@ -64,10 +75,10 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= -github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= github.com/dvsekhvalnov/jose2go v0.0.0-20180829124132-7f401d37b68a h1:mq+R6XEM6lJX5VlLyZIrUSP8tSuJp82xTK89hvBwJbU= github.com/dvsekhvalnov/jose2go v0.0.0-20180829124132-7f401d37b68a/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM= +github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= +github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= @@ -85,6 +96,7 @@ github.com/go-git/go-billy/v5 v5.0.0 h1:7NQHvd9FVid8VL4qVUMm8XifBK+2xCoZ2lSk0agR github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= github.com/go-git/go-git-fixtures/v4 v4.0.1 h1:q+IFMfLx200Q3scvt2hN79JsEzy4AmBTp/pqnefH+Bc= github.com/go-git/go-git-fixtures/v4 v4.0.1/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw= +github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw= github.com/go-git/go-git/v5 v5.1.0 h1:HxJn9g/E7eYvKW3Fm7Jt4ee8LXfPOm/H1cdDu8vEssk= github.com/go-git/go-git/v5 v5.1.0/go.mod h1:ZKfuPUoY1ZqIG4QG9BDBh3G4gLM5zvPuSJAozQrZuyM= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= diff --git a/repository/gogit_config.go b/repository/gogit_config.go index 0f91b092..000658a8 100644 --- a/repository/gogit_config.go +++ b/repository/gogit_config.go @@ -105,19 +105,40 @@ func (ggc *goGitConfig) ReadString(key string) (string, error) { split := strings.Split(key, ".") - // TODO: return ErrNoConfigEntry and ErrMultipleConfigEntry - // Can use forked go-git: https://github.com/go-git/go-git/pull/112 + if len(split) <= 1 { + return "", fmt.Errorf("invalid key") + } + + sectionName := split[0] + if !cfg.Raw.HasSection(sectionName) { + return "", ErrNoConfigEntry + } + section := cfg.Raw.Section(sectionName) switch { - case len(split) <= 1: - return "", fmt.Errorf("invalid key") case len(split) == 2: - return cfg.Raw.Section(split[0]).Option(split[1]), nil + optionName := split[1] + if !section.HasOption(optionName) { + return "", ErrNoConfigEntry + } + if len(section.OptionAll(optionName)) > 1 { + return "", ErrMultipleConfigEntry + } + return section.Option(optionName), nil default: - section := split[0] - subsection := strings.Join(split[1:len(split)-2], ".") - option := split[len(split)-1] - return cfg.Raw.Section(section).Subsection(subsection).Option(option), nil + subsectionName := strings.Join(split[1:len(split)-2], ".") + optionName := split[len(split)-1] + if !section.HasSubsection(subsectionName) { + return "", ErrNoConfigEntry + } + subsection := section.Subsection(subsectionName) + if !subsection.HasOption(optionName) { + return "", ErrNoConfigEntry + } + if len(subsection.OptionAll(optionName)) > 1 { + return "", ErrMultipleConfigEntry + } + return subsection.Option(optionName), nil } } @@ -137,16 +158,6 @@ func (ggc *goGitConfig) RemoveAll(keyPrefix string) error { split := strings.Split(keyPrefix, ".") - // missing in go-git - hasOption := func(options config.Options, key string) bool { - for _, option := range options { - if option.IsKey(key) { - return true - } - } - return false - } - switch { case len(split) < 1: return fmt.Errorf("invalid key prefix") @@ -163,7 +174,7 @@ func (ggc *goGitConfig) RemoveAll(keyPrefix string) error { if cfg.Raw.Section(section).HasSubsection(rest) { cfg.Raw.RemoveSubsection(section, rest) } else { - if hasOption(cfg.Raw.Section(section).Options, rest) { + if cfg.Raw.Section(section).HasOption(rest) { cfg.Raw.Section(section).RemoveOption(rest) } else { return fmt.Errorf("invalid key prefix") From 2bda70316de41db59695aa3bff69ffd930283387 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Wed, 2 Sep 2020 14:48:28 +0200 Subject: [PATCH 06/25] repository: more go-git implementation --- repository/gogit.go | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/repository/gogit.go b/repository/gogit.go index f115fab5..35283706 100644 --- a/repository/gogit.go +++ b/repository/gogit.go @@ -12,6 +12,8 @@ import ( gogit "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/config" "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/filemode" + "github.com/go-git/go-git/v5/plumbing/object" "github.com/MichaelMure/git-bug/util/lamport" ) @@ -285,10 +287,37 @@ func (repo *GoGitRepo) ReadData(hash Hash) ([]byte, error) { } func (repo *GoGitRepo) StoreTree(mapping []TreeEntry) (Hash, error) { - panic("implement me") + var tree object.Tree + + for _, entry := range mapping { + mode := filemode.Regular + if entry.ObjectType == Tree { + mode = filemode.Dir + } + + tree.Entries = append(tree.Entries, object.TreeEntry{ + Name: entry.Name, + Mode: mode, + Hash: plumbing.NewHash(entry.Hash.String()), + }) + } + + obj := repo.r.Storer.NewEncodedObject() + err := tree.Encode(obj) + if err != nil { + return "", err + } + + hash, err := repo.r.Storer.SetEncodedObject(obj) + if err != nil { + return "", err + } + + return Hash(hash.String()), nil } func (repo *GoGitRepo) ReadTree(hash Hash) ([]TreeEntry, error) { + // repo.r.TreeObject() panic("implement me") } @@ -313,7 +342,7 @@ func (repo *GoGitRepo) UpdateRef(ref string, hash Hash) error { } func (repo *GoGitRepo) RemoveRef(ref string) error { - panic("implement me") + return repo.r.Storer.RemoveReference(plumbing.ReferenceName(ref)) } func (repo *GoGitRepo) ListRefs(refspec string) ([]string, error) { From e442ed36b37654108d45862ca3b31175dc860510 Mon Sep 17 00:00:00 2001 From: vince Date: Tue, 8 Sep 2020 11:00:36 +0800 Subject: [PATCH 07/25] Add more function implementations --- repository/gogit.go | 122 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 111 insertions(+), 11 deletions(-) diff --git a/repository/gogit.go b/repository/gogit.go index 35283706..bd8ada33 100644 --- a/repository/gogit.go +++ b/repository/gogit.go @@ -8,6 +8,7 @@ import ( stdpath "path" "path/filepath" "sync" + "time" gogit "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/config" @@ -303,6 +304,7 @@ func (repo *GoGitRepo) StoreTree(mapping []TreeEntry) (Hash, error) { } obj := repo.r.Storer.NewEncodedObject() + obj.SetType(plumbing.TreeObject) err := tree.Encode(obj) if err != nil { return "", err @@ -317,28 +319,101 @@ func (repo *GoGitRepo) StoreTree(mapping []TreeEntry) (Hash, error) { } func (repo *GoGitRepo) ReadTree(hash Hash) ([]TreeEntry, error) { - // repo.r.TreeObject() - panic("implement me") + obj, err := repo.r.TreeObject(plumbing.NewHash(hash.String())) + if err != nil { + return nil, err + } + + treeEntries := make([]TreeEntry, len(obj.Entries)) + for i, entry := range obj.Entries { + objType := Blob + if entry.Mode == filemode.Dir { + objType = Tree + } + + treeEntries[i] = TreeEntry{ + ObjectType: objType, + Hash: Hash(entry.Hash.String()), + Name: entry.Name, + } + } + + return treeEntries, nil } func (repo *GoGitRepo) StoreCommit(treeHash Hash) (Hash, error) { - panic("implement me") + return repo.StoreCommitWithParent(treeHash, "") } func (repo *GoGitRepo) StoreCommitWithParent(treeHash Hash, parent Hash) (Hash, error) { - panic("implement me") + cfg, err := repo.r.Config() + if err != nil { + return "", err + } + + commit := object.Commit{ + Author: object.Signature{ + cfg.Author.Name, + cfg.Author.Email, + time.Now(), + }, + Committer: object.Signature{ + cfg.Committer.Name, + cfg.Committer.Email, + time.Now(), + }, + Message: "", + TreeHash: plumbing.NewHash(treeHash.String()), + } + + if parent != "" { + commit.ParentHashes = []plumbing.Hash{plumbing.NewHash(parent.String())} + } + + obj := repo.r.Storer.NewEncodedObject() + obj.SetType(plumbing.CommitObject) + err = commit.Encode(obj) + if err != nil { + return "", err + } + + hash, err := repo.r.Storer.SetEncodedObject(obj) + if err != nil { + return "", err + } + + return Hash(hash.String()), nil } func (repo *GoGitRepo) GetTreeHash(commit Hash) (Hash, error) { - panic("implement me") + obj, err := repo.r.CommitObject(plumbing.NewHash(commit.String())) + if err != nil { + return "", err + } + + return Hash(obj.TreeHash.String()), nil } func (repo *GoGitRepo) FindCommonAncestor(commit1 Hash, commit2 Hash) (Hash, error) { - panic("implement me") + obj1, err := repo.r.CommitObject(plumbing.NewHash(commit1.String())) + if err != nil { + return "", err + } + obj2, err := repo.r.CommitObject(plumbing.NewHash(commit2.String())) + if err != nil { + return "", err + } + + commits, err := obj1.MergeBase(obj2) + if err != nil { + return "", err + } + + return Hash(commits[0].Hash.String()), nil } func (repo *GoGitRepo) UpdateRef(ref string, hash Hash) error { - panic("implement me") + return repo.r.Storer.SetReference(plumbing.NewHashReference(plumbing.ReferenceName(ref), plumbing.NewHash(hash.String()))) } func (repo *GoGitRepo) RemoveRef(ref string) error { @@ -346,19 +421,44 @@ func (repo *GoGitRepo) RemoveRef(ref string) error { } func (repo *GoGitRepo) ListRefs(refspec string) ([]string, error) { - panic("implement me") + refIter, err := repo.r.References() + if err != nil { + return nil, err + } + + refs := make([]string, 0) + + for ref, _ := refIter.Next(); ref != nil; { + refs = append(refs, ref.String()) // TODO: Use format to search + } + return refs, nil } func (repo *GoGitRepo) RefExist(ref string) (bool, error) { - panic("implement me") + _, err := repo.r.Reference(plumbing.ReferenceName(ref), false) + if err == nil { + return true, nil + } else if err == plumbing.ErrReferenceNotFound { + return false, nil + } + return false, err } func (repo *GoGitRepo) CopyRef(source string, dest string) error { - panic("implement me") + return repo.r.Storer.SetReference(plumbing.NewHashReference(plumbing.ReferenceName(dest), plumbing.NewHash(source))) } func (repo *GoGitRepo) ListCommits(ref string) ([]Hash, error) { - panic("implement me") + commitIter, err := repo.r.CommitObjects() + if err != nil { + return nil, err + } + + var commits []Hash // TODO: Implement refspec + for commit, _ := commitIter.Next(); commit != nil; { + commits = append(commits, Hash(commit.Hash.String())) + } + return commits, nil } // GetOrCreateClock return a Lamport clock stored in the Repo. From bde93756321b7a5eb0ec013e059914528b7c2610 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Tue, 8 Sep 2020 11:57:42 +0200 Subject: [PATCH 08/25] bump minimal go version to 1.13 --- .travis.yml | 6 +++--- go.mod | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index cb86428d..a64ed083 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,11 @@ matrix: include: - - language: go - go: 1.12.x - language: go go: 1.13.x - language: go go: 1.14.x + - language: go + go: 1.15.x - language: node_js node_js: node before_install: @@ -41,5 +41,5 @@ deploy: file: dist/**/* on: repo: MichaelMure/git-bug - go: 1.13.x + go: 1.14.x tags: true diff --git a/go.mod b/go.mod index cb22412c..0129cbd4 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/MichaelMure/git-bug -go 1.12 +go 1.13 require ( github.com/99designs/gqlgen v0.10.3-0.20200209012558-b7a58a1c0e4b From d4f1d5659b9d23ff0768ed584ae320cb657c6f2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Tue, 8 Sep 2020 14:31:40 +0200 Subject: [PATCH 09/25] repo: fix some go-git implementation --- identity/identity_actions.go | 3 ++- repository/git.go | 4 ++-- repository/gogit.go | 24 +++++++++++++++++++----- repository/mock_repo.go | 4 ++-- repository/repo.go | 2 +- 5 files changed, 26 insertions(+), 11 deletions(-) diff --git a/identity/identity_actions.go b/identity/identity_actions.go index aa6a2a91..e33b75f9 100644 --- a/identity/identity_actions.go +++ b/identity/identity_actions.go @@ -4,9 +4,10 @@ import ( "fmt" "strings" + "github.com/pkg/errors" + "github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/repository" - "github.com/pkg/errors" ) // Fetch retrieve updates from a remote diff --git a/repository/git.go b/repository/git.go index 85107ba5..37b79556 100644 --- a/repository/git.go +++ b/repository/git.go @@ -302,8 +302,8 @@ func (repo *GitRepo) RemoveRef(ref string) error { } // ListRefs will return a list of Git ref matching the given refspec -func (repo *GitRepo) ListRefs(refspec string) ([]string, error) { - stdout, err := repo.runGitCommand("for-each-ref", "--format=%(refname)", refspec) +func (repo *GitRepo) ListRefs(refPrefix string) ([]string, error) { + stdout, err := repo.runGitCommand("for-each-ref", "--format=%(refname)", refPrefix) if err != nil { return nil, err diff --git a/repository/gogit.go b/repository/gogit.go index bd8ada33..78bae1f9 100644 --- a/repository/gogit.go +++ b/repository/gogit.go @@ -7,6 +7,7 @@ import ( "os" stdpath "path" "path/filepath" + "strings" "sync" "time" @@ -420,7 +421,7 @@ func (repo *GoGitRepo) RemoveRef(ref string) error { return repo.r.Storer.RemoveReference(plumbing.ReferenceName(ref)) } -func (repo *GoGitRepo) ListRefs(refspec string) ([]string, error) { +func (repo *GoGitRepo) ListRefs(refPrefix string) ([]string, error) { refIter, err := repo.r.References() if err != nil { return nil, err @@ -428,9 +429,16 @@ func (repo *GoGitRepo) ListRefs(refspec string) ([]string, error) { refs := make([]string, 0) - for ref, _ := refIter.Next(); ref != nil; { - refs = append(refs, ref.String()) // TODO: Use format to search + err = refIter.ForEach(func(ref *plumbing.Reference) error { + if strings.HasPrefix(ref.Name().String(), refPrefix) { + refs = append(refs, ref.Name().String()) + } + return nil + }) + if err != nil { + return nil, err } + return refs, nil } @@ -454,10 +462,16 @@ func (repo *GoGitRepo) ListCommits(ref string) ([]Hash, error) { return nil, err } - var commits []Hash // TODO: Implement refspec - for commit, _ := commitIter.Next(); commit != nil; { + var commits []Hash + + err = commitIter.ForEach(func(commit *object.Commit) error { commits = append(commits, Hash(commit.Hash.String())) + return nil + }) + if err != nil { + return nil, err } + return commits, nil } diff --git a/repository/mock_repo.go b/repository/mock_repo.go index b3b4cb41..4e2c89bc 100644 --- a/repository/mock_repo.go +++ b/repository/mock_repo.go @@ -164,11 +164,11 @@ func (r *mockRepoForTest) CopyRef(source string, dest string) error { return nil } -func (r *mockRepoForTest) ListRefs(refspec string) ([]string, error) { +func (r *mockRepoForTest) ListRefs(refPrefix string) ([]string, error) { var keys []string for k := range r.refs { - if strings.HasPrefix(k, refspec) { + if strings.HasPrefix(k, refPrefix) { keys = append(keys, k) } } diff --git a/repository/repo.go b/repository/repo.go index 806edace..3deb6f1b 100644 --- a/repository/repo.go +++ b/repository/repo.go @@ -91,7 +91,7 @@ type RepoData interface { RemoveRef(ref string) error // ListRefs will return a list of Git ref matching the given refspec - ListRefs(refspec string) ([]string, error) + ListRefs(refPrefix string) ([]string, error) // RefExist will check if a reference exist in Git RefExist(ref string) (bool, error) From 27e70af23692a70fcb129370235e46eaf33b8cfd Mon Sep 17 00:00:00 2001 From: vince Date: Tue, 8 Sep 2020 20:45:03 +0800 Subject: [PATCH 10/25] fix ListCommits implementation --- repository/gogit.go | 34 ++++++++++++++++++++++++++-------- repository/gogit_test.go | 2 +- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/repository/gogit.go b/repository/gogit.go index 78bae1f9..a93e8576 100644 --- a/repository/gogit.go +++ b/repository/gogit.go @@ -453,24 +453,42 @@ func (repo *GoGitRepo) RefExist(ref string) (bool, error) { } func (repo *GoGitRepo) CopyRef(source string, dest string) error { - return repo.r.Storer.SetReference(plumbing.NewHashReference(plumbing.ReferenceName(dest), plumbing.NewHash(source))) + r, err := repo.r.Reference(plumbing.ReferenceName(source), false) + if err != nil { + return err + } + return repo.r.Storer.SetReference(plumbing.NewHashReference(plumbing.ReferenceName(dest), r.Hash())) } func (repo *GoGitRepo) ListCommits(ref string) ([]Hash, error) { - commitIter, err := repo.r.CommitObjects() + r, err := repo.r.Reference(plumbing.ReferenceName(ref), false) if err != nil { return nil, err } - var commits []Hash - - err = commitIter.ForEach(func(commit *object.Commit) error { - commits = append(commits, Hash(commit.Hash.String())) - return nil - }) + commit, err := repo.r.CommitObject(r.Hash()) if err != nil { return nil, err } + commits := []Hash{Hash(commit.Hash.String())} + + for { + commit, err = commit.Parent(0) + + if err != nil { + if err == object.ErrParentNotFound { + break + } + + return nil, err + } + + if commit.NumParents() > 1 { + return nil, fmt.Errorf("multiple parents") + } + + commits = append(commits, Hash(commit.Hash.String())) + } return commits, nil } diff --git a/repository/gogit_test.go b/repository/gogit_test.go index 9dcf109f..fba990d3 100644 --- a/repository/gogit_test.go +++ b/repository/gogit_test.go @@ -58,7 +58,7 @@ func TestNewGoGitRepo(t *testing.T) { require.Error(t, err, i) } else { require.NoError(t, err, i) - assert.Equal(t, tc.outPath, r.GetPath(), i) + assert.Equal(t, filepath.ToSlash(tc.outPath), filepath.ToSlash(r.GetPath()), i) } } } From fb9170e2590531fddb5c2e0059dac5ac4bb64013 Mon Sep 17 00:00:00 2001 From: vince Date: Fri, 11 Sep 2020 08:24:33 +0800 Subject: [PATCH 11/25] fix go sum rebase artifacts --- go.sum | 3 --- 1 file changed, 3 deletions(-) diff --git a/go.sum b/go.sum index afbb0909..a9b8c971 100644 --- a/go.sum +++ b/go.sum @@ -7,8 +7,6 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/Masterminds/glide v0.13.2/go.mod h1:STyF5vcenH/rUqTEv+/hBXlSTo7KYwg2oc2f4tzPWic= github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/vcs v1.13.0/go.mod h1:N09YCmOQr6RLxC6UNHzuVwAdodYbbnycGHSmwVJjcKA= -<<<<<<< HEAD -======= github.com/MichaelMure/go-git/v5 v5.1.1-0.20200827115354-b40ca794fe33 h1:QFzkZPUMm0HRZ0dZ+GgDKHPUrgUrH3CbcyuzQlhBeww= github.com/MichaelMure/go-git/v5 v5.1.1-0.20200827115354-b40ca794fe33/go.mod h1:kh02eMX+wdqqxgNMEyq8YgwlIOsDOa9homkUq1PoTMs= github.com/MichaelMure/go-term-text v0.2.6 h1:dSmJSzk2iI5xWymSMrMbdVM1bxYWu3DjDFhdcJvAuqA= @@ -17,7 +15,6 @@ github.com/MichaelMure/go-term-text v0.2.7 h1:nSYvYGwXxJoiQu6kdGSErpxZ6ah/4WlJyp github.com/MichaelMure/go-term-text v0.2.7/go.mod h1:6z+q5b/nP1V8I9KkWQcUi5QpmF8DVrz9vLJ4hdoxHnM= github.com/MichaelMure/go-term-text v0.2.8 h1:daXIVPjPkAhcLhA+tfjQBHYjatb1D42/LY1Nw2PXYlU= github.com/MichaelMure/go-term-text v0.2.8/go.mod h1:6z+q5b/nP1V8I9KkWQcUi5QpmF8DVrz9vLJ4hdoxHnM= ->>>>>>> d2f92f6... repository: fix a todo in the gogit repo github.com/MichaelMure/go-term-text v0.2.9 h1:jUxInT3rDhl4WoJgLnmMS3hR79zigyJS1TqKFDTI6xE= github.com/MichaelMure/go-term-text v0.2.9/go.mod h1:2QSU/Nn2u41Tqoar+90RlYuhjngJPYgod7evnsYwkWc= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= From cdfbecf3e2b874f0b9dd99b75bcb80aa5be8b4b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sat, 26 Sep 2020 14:05:11 +0200 Subject: [PATCH 12/25] repo: fix gogit clock path --- repository/gogit.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repository/gogit.go b/repository/gogit.go index a93e8576..b0a4672b 100644 --- a/repository/gogit.go +++ b/repository/gogit.go @@ -507,7 +507,7 @@ func (repo *GoGitRepo) GetOrCreateClock(name string) (lamport.Clock, error) { repo.clocksMutex.Lock() defer repo.clocksMutex.Unlock() - p := clockPath + name + "-clock" + p := stdpath.Join(repo.path, clockPath, name+"-clock") c, err = lamport.NewPersistedClock(p) if err != nil { @@ -526,7 +526,7 @@ func (repo *GoGitRepo) getClock(name string) (lamport.Clock, error) { return c, nil } - p := clockPath + name + "-clock" + p := stdpath.Join(repo.path, clockPath, name+"-clock") c, err := lamport.LoadPersistedClock(p) if err == nil { From cedcc2772c88b78bc62594afd06c44c7a7c244e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sat, 26 Sep 2020 14:06:01 +0200 Subject: [PATCH 13/25] repo: smaller interfaces --- repository/repo.go | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/repository/repo.go b/repository/repo.go index 3deb6f1b..6349007b 100644 --- a/repository/repo.go +++ b/repository/repo.go @@ -22,6 +22,12 @@ type Repo interface { RepoData } +// ClockedRepo is a Repo that also has Lamport clocks +type ClockedRepo interface { + Repo + RepoClock +} + // RepoConfig access the configuration of a repository type RepoConfig interface { // LocalConfig give access to the repository scoped configuration @@ -103,10 +109,8 @@ type RepoData interface { ListCommits(ref string) ([]Hash, error) } -// ClockedRepo is a Repo that also has Lamport clocks -type ClockedRepo interface { - Repo - +// RepoClock give access to Lamport clocks +type RepoClock interface { // GetOrCreateClock return a Lamport clock stored in the Repo. // If the clock doesn't exist, it's created. GetOrCreateClock(name string) (lamport.Clock, error) @@ -127,7 +131,11 @@ type ClockLoader struct { // TestedRepo is an extended ClockedRepo with function for testing only type TestedRepo interface { ClockedRepo + repoTest +} +// repoTest give access to test only functions +type repoTest interface { // AddRemote add a new remote to the repository AddRemote(name string, url string) error } From 9408f1eb0e99cddee1a7e1739bd786de543c30e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sat, 26 Sep 2020 14:06:17 +0200 Subject: [PATCH 14/25] repo: test both plain and bare, test clocks --- repository/repo_testing.go | 230 +++++++++++++++++++++---------------- 1 file changed, 130 insertions(+), 100 deletions(-) diff --git a/repository/repo_testing.go b/repository/repo_testing.go index 28eb9a21..53375a5b 100644 --- a/repository/repo_testing.go +++ b/repository/repo_testing.go @@ -7,8 +7,9 @@ import ( "strings" "testing" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/MichaelMure/git-bug/util/lamport" ) func CleanupTestRepos(repos ...Repo) { @@ -47,136 +48,165 @@ type RepoCleaner func(repos ...Repo) // Test suite for a Repo implementation func RepoTest(t *testing.T, creator RepoCreator, cleaner RepoCleaner) { - t.Run("Blob-Tree-Commit-Ref", func(t *testing.T) { - repo := creator(false) - defer cleaner(repo) + for bare, name := range map[bool]string{ + false: "Plain", + true: "Bare", + } { + t.Run(name, func(t *testing.T) { + t.Run("Blob-Tree-Commit-Ref", func(t *testing.T) { + repo := creator(bare) + defer cleaner(repo) - // Blob + // Blob - data := randomData() + data := randomData() - blobHash1, err := repo.StoreData(data) - require.NoError(t, err) - assert.True(t, blobHash1.IsValid()) + blobHash1, err := repo.StoreData(data) + require.NoError(t, err) + require.True(t, blobHash1.IsValid()) - blob1Read, err := repo.ReadData(blobHash1) - require.NoError(t, err) - assert.Equal(t, data, blob1Read) + blob1Read, err := repo.ReadData(blobHash1) + require.NoError(t, err) + require.Equal(t, data, blob1Read) - // Tree + // Tree - blobHash2, err := repo.StoreData(randomData()) - require.NoError(t, err) - blobHash3, err := repo.StoreData(randomData()) - require.NoError(t, err) + blobHash2, err := repo.StoreData(randomData()) + require.NoError(t, err) + blobHash3, err := repo.StoreData(randomData()) + require.NoError(t, err) - tree1 := []TreeEntry{ - { - ObjectType: Blob, - Hash: blobHash1, - Name: "blob1", - }, - { - ObjectType: Blob, - Hash: blobHash2, - Name: "blob2", - }, - } + tree1 := []TreeEntry{ + { + ObjectType: Blob, + Hash: blobHash1, + Name: "blob1", + }, + { + ObjectType: Blob, + Hash: blobHash2, + Name: "blob2", + }, + } - treeHash1, err := repo.StoreTree(tree1) - require.NoError(t, err) - assert.True(t, treeHash1.IsValid()) + treeHash1, err := repo.StoreTree(tree1) + require.NoError(t, err) + require.True(t, treeHash1.IsValid()) - tree1Read, err := repo.ReadTree(treeHash1) - require.NoError(t, err) - assert.ElementsMatch(t, tree1, tree1Read) + tree1Read, err := repo.ReadTree(treeHash1) + require.NoError(t, err) + require.ElementsMatch(t, tree1, tree1Read) - tree2 := []TreeEntry{ - { - ObjectType: Tree, - Hash: treeHash1, - Name: "tree1", - }, - { - ObjectType: Blob, - Hash: blobHash3, - Name: "blob3", - }, - } + tree2 := []TreeEntry{ + { + ObjectType: Tree, + Hash: treeHash1, + Name: "tree1", + }, + { + ObjectType: Blob, + Hash: blobHash3, + Name: "blob3", + }, + } - treeHash2, err := repo.StoreTree(tree2) - require.NoError(t, err) - assert.True(t, treeHash2.IsValid()) + treeHash2, err := repo.StoreTree(tree2) + require.NoError(t, err) + require.True(t, treeHash2.IsValid()) - tree2Read, err := repo.ReadTree(treeHash2) - require.NoError(t, err) - assert.ElementsMatch(t, tree2, tree2Read) + tree2Read, err := repo.ReadTree(treeHash2) + require.NoError(t, err) + require.ElementsMatch(t, tree2, tree2Read) - // Commit + // Commit - commit1, err := repo.StoreCommit(treeHash1) - require.NoError(t, err) - assert.True(t, commit1.IsValid()) + commit1, err := repo.StoreCommit(treeHash1) + require.NoError(t, err) + require.True(t, commit1.IsValid()) - treeHash1Read, err := repo.GetTreeHash(commit1) - require.NoError(t, err) - assert.Equal(t, treeHash1, treeHash1Read) + treeHash1Read, err := repo.GetTreeHash(commit1) + require.NoError(t, err) + require.Equal(t, treeHash1, treeHash1Read) - commit2, err := repo.StoreCommitWithParent(treeHash2, commit1) - require.NoError(t, err) - assert.True(t, commit2.IsValid()) + commit2, err := repo.StoreCommitWithParent(treeHash2, commit1) + require.NoError(t, err) + require.True(t, commit2.IsValid()) - treeHash2Read, err := repo.GetTreeHash(commit2) - require.NoError(t, err) - assert.Equal(t, treeHash2, treeHash2Read) + treeHash2Read, err := repo.GetTreeHash(commit2) + require.NoError(t, err) + require.Equal(t, treeHash2, treeHash2Read) - // Ref + // Ref - exist1, err := repo.RefExist("refs/bugs/ref1") - require.NoError(t, err) - assert.False(t, exist1) + exist1, err := repo.RefExist("refs/bugs/ref1") + require.NoError(t, err) + require.False(t, exist1) - err = repo.UpdateRef("refs/bugs/ref1", commit2) - require.NoError(t, err) + err = repo.UpdateRef("refs/bugs/ref1", commit2) + require.NoError(t, err) - exist1, err = repo.RefExist("refs/bugs/ref1") - require.NoError(t, err) - assert.True(t, exist1) + exist1, err = repo.RefExist("refs/bugs/ref1") + require.NoError(t, err) + require.True(t, exist1) - ls, err := repo.ListRefs("refs/bugs") - require.NoError(t, err) - assert.ElementsMatch(t, []string{"refs/bugs/ref1"}, ls) + ls, err := repo.ListRefs("refs/bugs") + require.NoError(t, err) + require.ElementsMatch(t, []string{"refs/bugs/ref1"}, ls) - err = repo.CopyRef("refs/bugs/ref1", "refs/bugs/ref2") - require.NoError(t, err) + err = repo.CopyRef("refs/bugs/ref1", "refs/bugs/ref2") + require.NoError(t, err) - ls, err = repo.ListRefs("refs/bugs") - require.NoError(t, err) - assert.ElementsMatch(t, []string{"refs/bugs/ref1", "refs/bugs/ref2"}, ls) + ls, err = repo.ListRefs("refs/bugs") + require.NoError(t, err) + require.ElementsMatch(t, []string{"refs/bugs/ref1", "refs/bugs/ref2"}, ls) - commits, err := repo.ListCommits("refs/bugs/ref2") - require.NoError(t, err) - assert.ElementsMatch(t, []Hash{commit1, commit2}, commits) + commits, err := repo.ListCommits("refs/bugs/ref2") + require.NoError(t, err) + require.ElementsMatch(t, []Hash{commit1, commit2}, commits) - // Graph + // Graph - commit3, err := repo.StoreCommitWithParent(treeHash1, commit1) - require.NoError(t, err) + commit3, err := repo.StoreCommitWithParent(treeHash1, commit1) + require.NoError(t, err) - ancestorHash, err := repo.FindCommonAncestor(commit2, commit3) - require.NoError(t, err) - assert.Equal(t, commit1, ancestorHash) + ancestorHash, err := repo.FindCommonAncestor(commit2, commit3) + require.NoError(t, err) + require.Equal(t, commit1, ancestorHash) - err = repo.RemoveRef("refs/bugs/ref1") - require.NoError(t, err) - }) + err = repo.RemoveRef("refs/bugs/ref1") + require.NoError(t, err) + }) - t.Run("Local config", func(t *testing.T) { - repo := creator(false) - defer cleaner(repo) + t.Run("Local config", func(t *testing.T) { + repo := creator(bare) + defer cleaner(repo) - testConfig(t, repo.LocalConfig()) - }) + testConfig(t, repo.LocalConfig()) + }) + + t.Run("Clocks", func(t *testing.T) { + repo := creator(bare) + defer cleaner(repo) + + clock, err := repo.GetOrCreateClock("foo") + require.NoError(t, err) + require.Equal(t, lamport.Time(1), clock.Time()) + + time, err := clock.Increment() + require.NoError(t, err) + require.Equal(t, lamport.Time(1), time) + require.Equal(t, lamport.Time(2), clock.Time()) + + clock2, err := repo.GetOrCreateClock("foo") + require.NoError(t, err) + require.Equal(t, lamport.Time(2), clock2.Time()) + + clock3, err := repo.GetOrCreateClock("bar") + require.NoError(t, err) + require.Equal(t, lamport.Time(1), clock3.Time()) + }) + }) + } } func randomData() []byte { From c68be32df499fd06c2c0cc906f719586da9ee3f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sat, 26 Sep 2020 23:00:27 +0200 Subject: [PATCH 15/25] repo: split Config into 2 smaller interfaces --- repository/config.go | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/repository/config.go b/repository/config.go index 2133b169..70d51f11 100644 --- a/repository/config.go +++ b/repository/config.go @@ -13,15 +13,11 @@ var ( // Config represent the common function interacting with the repository config storage type Config interface { - // Store writes a single key/value pair in the config - StoreString(key, value string) error - - // Store writes a key and timestamp value to the config - StoreTimestamp(key string, value time.Time) error - - // Store writes a key and boolean value to the config - StoreBool(key string, value bool) error + ConfigRead + ConfigWrite +} +type ConfigRead interface { // ReadAll reads all key/value pair matching the key prefix ReadAll(keyPrefix string) (map[string]string, error) @@ -39,6 +35,17 @@ type Config interface { // Return ErrNoConfigEntry or ErrMultipleConfigEntry if // there is zero or more than one entry for this key ReadTimestamp(key string) (time.Time, error) +} + +type ConfigWrite interface { + // Store writes a single key/value pair in the config + StoreString(key, value string) error + + // Store writes a key and timestamp value to the config + StoreTimestamp(key string, value time.Time) error + + // Store writes a key and boolean value to the config + StoreBool(key string, value bool) error // RemoveAll removes all key/value pair matching the key prefix RemoveAll(keyPrefix string) error From aa8055e27e9b2b5e102681635fb73dfd05f08fa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sat, 26 Sep 2020 23:01:14 +0200 Subject: [PATCH 16/25] repo: split mocks into smaller reusable components --- repository/mock_repo.go | 212 +++++++++++++++++++++++++--------------- 1 file changed, 134 insertions(+), 78 deletions(-) diff --git a/repository/mock_repo.go b/repository/mock_repo.go index 4e2c89bc..07425765 100644 --- a/repository/mock_repo.go +++ b/repository/mock_repo.go @@ -15,92 +15,138 @@ var _ TestedRepo = &mockRepoForTest{} // mockRepoForTest defines an instance of Repo that can be used for testing. type mockRepoForTest struct { + *mockRepoConfig + *mockRepoKeyring + *mockRepoCommon + *mockRepoData + *mockRepoClock +} + +func NewMockRepoForTest() *mockRepoForTest { + return &mockRepoForTest{ + mockRepoConfig: NewMockRepoConfig(), + mockRepoKeyring: NewMockRepoKeyring(), + mockRepoCommon: NewMockRepoCommon(), + mockRepoData: NewMockRepoData(), + mockRepoClock: NewMockRepoClock(), + } +} + +var _ RepoConfig = &mockRepoConfig{} + +type mockRepoConfig struct { config *MemConfig globalConfig *MemConfig - keyring *keyring.ArrayKeyring - blobs map[Hash][]byte - trees map[Hash]string - commits map[Hash]commit - refs map[string]Hash - clocks map[string]lamport.Clock } +func NewMockRepoConfig() *mockRepoConfig { + return &mockRepoConfig{ + config: NewMemConfig(), + globalConfig: NewMemConfig(), + } +} + +// LocalConfig give access to the repository scoped configuration +func (r *mockRepoConfig) LocalConfig() Config { + return r.config +} + +// GlobalConfig give access to the git global configuration +func (r *mockRepoConfig) GlobalConfig() Config { + return r.globalConfig +} + +var _ RepoKeyring = &mockRepoKeyring{} + +type mockRepoKeyring struct { + keyring *keyring.ArrayKeyring +} + +func NewMockRepoKeyring() *mockRepoKeyring { + return &mockRepoKeyring{ + keyring: keyring.NewArrayKeyring(nil), + } +} + +// Keyring give access to a user-wide storage for secrets +func (r *mockRepoKeyring) Keyring() Keyring { + return r.keyring +} + +var _ RepoCommon = &mockRepoCommon{} + +type mockRepoCommon struct{} + +func NewMockRepoCommon() *mockRepoCommon { + return &mockRepoCommon{} +} + +// GetPath returns the path to the repo. +func (r *mockRepoCommon) GetPath() string { + return "~/mockRepo/" +} + +func (r *mockRepoCommon) GetUserName() (string, error) { + return "René Descartes", nil +} + +// GetUserEmail returns the email address that the user has used to configure git. +func (r *mockRepoCommon) GetUserEmail() (string, error) { + return "user@example.com", nil +} + +// GetCoreEditor returns the name of the editor that the user has used to configure git. +func (r *mockRepoCommon) GetCoreEditor() (string, error) { + return "vi", nil +} + +// GetRemotes returns the configured remotes repositories. +func (r *mockRepoCommon) GetRemotes() (map[string]string, error) { + return map[string]string{ + "origin": "git://github.com/MichaelMure/git-bug", + }, nil +} + +var _ RepoData = &mockRepoData{} + type commit struct { treeHash Hash parent Hash } -func NewMockRepoForTest() *mockRepoForTest { - return &mockRepoForTest{ - config: NewMemConfig(), - globalConfig: NewMemConfig(), - keyring: keyring.NewArrayKeyring(nil), - blobs: make(map[Hash][]byte), - trees: make(map[Hash]string), - commits: make(map[Hash]commit), - refs: make(map[string]Hash), - clocks: make(map[string]lamport.Clock), +type mockRepoData struct { + blobs map[Hash][]byte + trees map[Hash]string + commits map[Hash]commit + refs map[string]Hash +} + +func NewMockRepoData() *mockRepoData { + return &mockRepoData{ + blobs: make(map[Hash][]byte), + trees: make(map[Hash]string), + commits: make(map[Hash]commit), + refs: make(map[string]Hash), } } -// LocalConfig give access to the repository scoped configuration -func (r *mockRepoForTest) LocalConfig() Config { - return r.config -} - -// GlobalConfig give access to the git global configuration -func (r *mockRepoForTest) GlobalConfig() Config { - return r.globalConfig -} - -// Keyring give access to a user-wide storage for secrets -func (r *mockRepoForTest) Keyring() Keyring { - return r.keyring -} - -// GetPath returns the path to the repo. -func (r *mockRepoForTest) GetPath() string { - return "~/mockRepo/" -} - -func (r *mockRepoForTest) GetUserName() (string, error) { - return "René Descartes", nil -} - -// GetUserEmail returns the email address that the user has used to configure git. -func (r *mockRepoForTest) GetUserEmail() (string, error) { - return "user@example.com", nil -} - -// GetCoreEditor returns the name of the editor that the user has used to configure git. -func (r *mockRepoForTest) GetCoreEditor() (string, error) { - return "vi", nil -} - -// GetRemotes returns the configured remotes repositories. -func (r *mockRepoForTest) GetRemotes() (map[string]string, error) { - return map[string]string{ - "origin": "git://github.com/MichaelMure/git-bug", - }, nil -} - // PushRefs push git refs to a remote -func (r *mockRepoForTest) PushRefs(remote string, refSpec string) (string, error) { +func (r *mockRepoData) PushRefs(remote string, refSpec string) (string, error) { return "", nil } -func (r *mockRepoForTest) FetchRefs(remote string, refSpec string) (string, error) { +func (r *mockRepoData) FetchRefs(remote string, refSpec string) (string, error) { return "", nil } -func (r *mockRepoForTest) StoreData(data []byte) (Hash, error) { +func (r *mockRepoData) StoreData(data []byte) (Hash, error) { rawHash := sha1.Sum(data) hash := Hash(fmt.Sprintf("%x", rawHash)) r.blobs[hash] = data return hash, nil } -func (r *mockRepoForTest) ReadData(hash Hash) ([]byte, error) { +func (r *mockRepoData) ReadData(hash Hash) ([]byte, error) { data, ok := r.blobs[hash] if !ok { @@ -110,7 +156,7 @@ func (r *mockRepoForTest) ReadData(hash Hash) ([]byte, error) { return data, nil } -func (r *mockRepoForTest) StoreTree(entries []TreeEntry) (Hash, error) { +func (r *mockRepoData) StoreTree(entries []TreeEntry) (Hash, error) { buffer := prepareTreeEntries(entries) rawHash := sha1.Sum(buffer.Bytes()) hash := Hash(fmt.Sprintf("%x", rawHash)) @@ -119,7 +165,7 @@ func (r *mockRepoForTest) StoreTree(entries []TreeEntry) (Hash, error) { return hash, nil } -func (r *mockRepoForTest) StoreCommit(treeHash Hash) (Hash, error) { +func (r *mockRepoData) StoreCommit(treeHash Hash) (Hash, error) { rawHash := sha1.Sum([]byte(treeHash)) hash := Hash(fmt.Sprintf("%x", rawHash)) r.commits[hash] = commit{ @@ -128,7 +174,7 @@ func (r *mockRepoForTest) StoreCommit(treeHash Hash) (Hash, error) { return hash, nil } -func (r *mockRepoForTest) StoreCommitWithParent(treeHash Hash, parent Hash) (Hash, error) { +func (r *mockRepoData) StoreCommitWithParent(treeHash Hash, parent Hash) (Hash, error) { rawHash := sha1.Sum([]byte(treeHash + parent)) hash := Hash(fmt.Sprintf("%x", rawHash)) r.commits[hash] = commit{ @@ -138,22 +184,22 @@ func (r *mockRepoForTest) StoreCommitWithParent(treeHash Hash, parent Hash) (Has return hash, nil } -func (r *mockRepoForTest) UpdateRef(ref string, hash Hash) error { +func (r *mockRepoData) UpdateRef(ref string, hash Hash) error { r.refs[ref] = hash return nil } -func (r *mockRepoForTest) RemoveRef(ref string) error { +func (r *mockRepoData) RemoveRef(ref string) error { delete(r.refs, ref) return nil } -func (r *mockRepoForTest) RefExist(ref string) (bool, error) { +func (r *mockRepoData) RefExist(ref string) (bool, error) { _, exist := r.refs[ref] return exist, nil } -func (r *mockRepoForTest) CopyRef(source string, dest string) error { +func (r *mockRepoData) CopyRef(source string, dest string) error { hash, exist := r.refs[source] if !exist { @@ -164,7 +210,7 @@ func (r *mockRepoForTest) CopyRef(source string, dest string) error { return nil } -func (r *mockRepoForTest) ListRefs(refPrefix string) ([]string, error) { +func (r *mockRepoData) ListRefs(refPrefix string) ([]string, error) { var keys []string for k := range r.refs { @@ -176,7 +222,7 @@ func (r *mockRepoForTest) ListRefs(refPrefix string) ([]string, error) { return keys, nil } -func (r *mockRepoForTest) ListCommits(ref string) ([]Hash, error) { +func (r *mockRepoData) ListCommits(ref string) ([]Hash, error) { var hashes []Hash hash := r.refs[ref] @@ -195,7 +241,7 @@ func (r *mockRepoForTest) ListCommits(ref string) ([]Hash, error) { return hashes, nil } -func (r *mockRepoForTest) ReadTree(hash Hash) ([]TreeEntry, error) { +func (r *mockRepoData) ReadTree(hash Hash) ([]TreeEntry, error) { var data string data, ok := r.trees[hash] @@ -218,7 +264,7 @@ func (r *mockRepoForTest) ReadTree(hash Hash) ([]TreeEntry, error) { return readTreeEntries(data) } -func (r *mockRepoForTest) FindCommonAncestor(hash1 Hash, hash2 Hash) (Hash, error) { +func (r *mockRepoData) FindCommonAncestor(hash1 Hash, hash2 Hash) (Hash, error) { ancestor1 := []Hash{hash1} for hash1 != "" { @@ -250,7 +296,7 @@ func (r *mockRepoForTest) FindCommonAncestor(hash1 Hash, hash2 Hash) (Hash, erro } } -func (r *mockRepoForTest) GetTreeHash(commit Hash) (Hash, error) { +func (r *mockRepoData) GetTreeHash(commit Hash) (Hash, error) { c, ok := r.commits[commit] if !ok { return "", fmt.Errorf("unknown commit") @@ -259,7 +305,21 @@ func (r *mockRepoForTest) GetTreeHash(commit Hash) (Hash, error) { return c.treeHash, nil } -func (r *mockRepoForTest) GetOrCreateClock(name string) (lamport.Clock, error) { +func (r *mockRepoData) AddRemote(name string, url string) error { + panic("implement me") +} + +type mockRepoClock struct { + clocks map[string]lamport.Clock +} + +func NewMockRepoClock() *mockRepoClock { + return &mockRepoClock{ + clocks: make(map[string]lamport.Clock), + } +} + +func (r *mockRepoClock) GetOrCreateClock(name string) (lamport.Clock, error) { if c, ok := r.clocks[name]; ok { return c, nil } @@ -268,7 +328,3 @@ func (r *mockRepoForTest) GetOrCreateClock(name string) (lamport.Clock, error) { r.clocks[name] = c return c, nil } - -func (r *mockRepoForTest) AddRemote(name string, url string) error { - panic("implement me") -} From c87e9abacfbdc4f221e2e328d4b229d6191f42e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sat, 26 Sep 2020 23:01:37 +0200 Subject: [PATCH 17/25] repo: only use the file backend for the keyring --- repository/keyring.go | 31 ++++--------------------------- 1 file changed, 4 insertions(+), 27 deletions(-) diff --git a/repository/keyring.go b/repository/keyring.go index 9f8171db..f690b0b3 100644 --- a/repository/keyring.go +++ b/repository/keyring.go @@ -29,37 +29,14 @@ func defaultKeyring() (Keyring, error) { return nil, err } - backends := []keyring.BackendType{ - keyring.WinCredBackend, - keyring.KeychainBackend, - keyring.PassBackend, - keyring.FileBackend, - } - return keyring.Open(keyring.Config{ - // TODO: ideally this would not be there, it disable the freedesktop backend on linux - // due to https://github.com/99designs/keyring/issues/44 - AllowedBackends: backends, + // only use the file backend until https://github.com/99designs/keyring/issues/74 is resolved + AllowedBackends: []keyring.BackendType{ + keyring.FileBackend, + }, ServiceName: "git-bug", - // MacOS keychain - KeychainName: "git-bug", - KeychainTrustApplication: true, - - // KDE Wallet - KWalletAppID: "git-bug", - KWalletFolder: "git-bug", - - // Windows - WinCredPrefix: "git-bug", - - // freedesktop.org's Secret Service - LibSecretCollectionName: "git-bug", - - // Pass (https://www.passwordstore.org/) - PassPrefix: "git-bug", - // Fallback encrypted file FileDir: path.Join(ucd, "git-bug", "keyring"), // As we write the file in the user's config directory, this file should already be protected by the OS against From 71b7eb14010be0c7799b4d5394798c89e379891b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sun, 27 Sep 2020 00:54:14 +0200 Subject: [PATCH 18/25] repo: implement local/global/any config everywhere --- cache/repo_cache.go | 2 + cache/repo_cache_common.go | 10 ++ commands/webui.go | 2 +- repository/config.go | 64 ++++++++ repository/config_test.go | 54 +++++++ repository/git.go | 83 ++++------ repository/git_cli.go | 56 +++++++ repository/git_config.go | 18 +-- repository/gogit.go | 25 +++- repository/mock_repo.go | 11 +- repository/repo.go | 6 + repository/repo_testing.go | 299 +++++++++++++++++++------------------ 12 files changed, 413 insertions(+), 217 deletions(-) create mode 100644 repository/config_test.go create mode 100644 repository/git_cli.go diff --git a/cache/repo_cache.go b/cache/repo_cache.go index 563fac6b..eeb7fb90 100644 --- a/cache/repo_cache.go +++ b/cache/repo_cache.go @@ -25,6 +25,8 @@ const formatVersion = 2 const defaultMaxLoadedBugs = 1000 var _ repository.RepoCommon = &RepoCache{} +var _ repository.RepoConfig = &RepoCache{} +var _ repository.RepoKeyring = &RepoCache{} // RepoCache is a cache for a Repository. This cache has multiple functions: // diff --git a/cache/repo_cache_common.go b/cache/repo_cache_common.go index 1e330a4a..95e2f7bb 100644 --- a/cache/repo_cache_common.go +++ b/cache/repo_cache_common.go @@ -20,6 +20,16 @@ func (c *RepoCache) LocalConfig() repository.Config { return c.repo.LocalConfig() } +// GlobalConfig give access to the global scoped configuration +func (c *RepoCache) GlobalConfig() repository.Config { + return c.repo.GlobalConfig() +} + +// AnyConfig give access to a merged local/global configuration +func (c *RepoCache) AnyConfig() repository.ConfigRead { + return c.repo.AnyConfig() +} + func (c *RepoCache) Keyring() repository.Keyring { return c.repo.Keyring() } diff --git a/commands/webui.go b/commands/webui.go index 4d87a303..7e5fc752 100644 --- a/commands/webui.go +++ b/commands/webui.go @@ -139,7 +139,7 @@ func runWebUI(env *Env, opts webUIOptions, args []string) error { env.out.Printf("Graphql Playground: http://%s/playground\n", addr) env.out.Println("Press Ctrl+c to quit") - configOpen, err := env.repo.LocalConfig().ReadBool(webUIOpenConfigKey) + configOpen, err := env.repo.AnyConfig().ReadBool(webUIOpenConfigKey) if err == repository.ErrNoConfigEntry { // default to true configOpen = true diff --git a/repository/config.go b/repository/config.go index 70d51f11..4ea326b5 100644 --- a/repository/config.go +++ b/repository/config.go @@ -59,3 +59,67 @@ func ParseTimestamp(s string) (time.Time, error) { return time.Unix(int64(timestamp), 0), nil } + +// mergeConfig is a helper to easily support RepoConfig.AnyConfig() +// from two separate local and global Config +func mergeConfig(local ConfigRead, global ConfigRead) *mergedConfig { + return &mergedConfig{ + local: local, + global: global, + } +} + +var _ ConfigRead = &mergedConfig{} + +type mergedConfig struct { + local ConfigRead + global ConfigRead +} + +func (m *mergedConfig) ReadAll(keyPrefix string) (map[string]string, error) { + values, err := m.global.ReadAll(keyPrefix) + if err != nil { + return nil, err + } + locals, err := m.local.ReadAll(keyPrefix) + if err != nil { + return nil, err + } + for k, val := range locals { + values[k] = val + } + return values, nil +} + +func (m *mergedConfig) ReadBool(key string) (bool, error) { + v, err := m.local.ReadBool(key) + if err == nil { + return v, nil + } + if err != ErrNoConfigEntry && err != ErrMultipleConfigEntry { + return false, err + } + return m.global.ReadBool(key) +} + +func (m *mergedConfig) ReadString(key string) (string, error) { + val, err := m.local.ReadString(key) + if err == nil { + return val, nil + } + if err != ErrNoConfigEntry && err != ErrMultipleConfigEntry { + return "", err + } + return m.global.ReadString(key) +} + +func (m *mergedConfig) ReadTimestamp(key string) (time.Time, error) { + val, err := m.local.ReadTimestamp(key) + if err == nil { + return val, nil + } + if err != ErrNoConfigEntry && err != ErrMultipleConfigEntry { + return time.Time{}, err + } + return m.global.ReadTimestamp(key) +} diff --git a/repository/config_test.go b/repository/config_test.go new file mode 100644 index 00000000..2a763540 --- /dev/null +++ b/repository/config_test.go @@ -0,0 +1,54 @@ +package repository + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestMergedConfig(t *testing.T) { + local := NewMemConfig() + global := NewMemConfig() + merged := mergeConfig(local, global) + + require.NoError(t, global.StoreBool("bool", true)) + require.NoError(t, global.StoreString("string", "foo")) + require.NoError(t, global.StoreTimestamp("timestamp", time.Unix(1234, 0))) + + val1, err := merged.ReadBool("bool") + require.NoError(t, err) + require.Equal(t, val1, true) + + val2, err := merged.ReadString("string") + require.NoError(t, err) + require.Equal(t, val2, "foo") + + val3, err := merged.ReadTimestamp("timestamp") + require.NoError(t, err) + require.Equal(t, val3, time.Unix(1234, 0)) + + require.NoError(t, local.StoreBool("bool", false)) + require.NoError(t, local.StoreString("string", "bar")) + require.NoError(t, local.StoreTimestamp("timestamp", time.Unix(5678, 0))) + + val1, err = merged.ReadBool("bool") + require.NoError(t, err) + require.Equal(t, val1, false) + + val2, err = merged.ReadString("string") + require.NoError(t, err) + require.Equal(t, val2, "bar") + + val3, err = merged.ReadTimestamp("timestamp") + require.NoError(t, err) + require.Equal(t, val3, time.Unix(5678, 0)) + + all, err := merged.ReadAll("") + require.NoError(t, err) + require.Equal(t, all, map[string]string{ + "bool": "false", + "string": "bar", + "timestamp": "5678", + }) +} diff --git a/repository/git.go b/repository/git.go index 37b79556..dba2d29d 100644 --- a/repository/git.go +++ b/repository/git.go @@ -4,8 +4,6 @@ package repository import ( "bytes" "fmt" - "io" - "os/exec" "path" "strings" "sync" @@ -22,6 +20,7 @@ var _ TestedRepo = &GitRepo{} // GitRepo represents an instance of a (local) git repository. type GitRepo struct { + gitCli path string clocksMutex sync.Mutex @@ -30,62 +29,6 @@ type GitRepo struct { keyring Keyring } -// LocalConfig give access to the repository scoped configuration -func (repo *GitRepo) LocalConfig() Config { - return newGitConfig(repo, false) -} - -// GlobalConfig give access to the git global configuration -func (repo *GitRepo) GlobalConfig() Config { - return newGitConfig(repo, true) -} - -func (repo *GitRepo) Keyring() Keyring { - return repo.keyring -} - -// Run the given git command with the given I/O reader/writers, returning an error if it fails. -func (repo *GitRepo) runGitCommandWithIO(stdin io.Reader, stdout, stderr io.Writer, args ...string) error { - // make sure that the working directory for the command - // always exist, in particular when running "git init". - path := strings.TrimSuffix(repo.path, ".git") - - // fmt.Printf("[%s] Running git %s\n", path, strings.Join(args, " ")) - - cmd := exec.Command("git", args...) - cmd.Dir = path - cmd.Stdin = stdin - cmd.Stdout = stdout - cmd.Stderr = stderr - - return cmd.Run() -} - -// Run the given git command and return its stdout, or an error if the command fails. -func (repo *GitRepo) runGitCommandRaw(stdin io.Reader, args ...string) (string, string, error) { - var stdout bytes.Buffer - var stderr bytes.Buffer - err := repo.runGitCommandWithIO(stdin, &stdout, &stderr, args...) - return strings.TrimSpace(stdout.String()), strings.TrimSpace(stderr.String()), err -} - -// Run the given git command and return its stdout, or an error if the command fails. -func (repo *GitRepo) runGitCommandWithStdin(stdin io.Reader, args ...string) (string, error) { - stdout, stderr, err := repo.runGitCommandRaw(stdin, args...) - if err != nil { - if stderr == "" { - stderr = "Error running git command: " + strings.Join(args, " ") - } - err = fmt.Errorf(stderr) - } - return stdout, err -} - -// Run the given git command and return its stdout, or an error if the command fails. -func (repo *GitRepo) runGitCommand(args ...string) (string, error) { - return repo.runGitCommandWithStdin(nil, args...) -} - // NewGitRepo determines if the given working directory is inside of a git repository, // and returns the corresponding GitRepo instance if it is. func NewGitRepo(path string, clockLoaders []ClockLoader) (*GitRepo, error) { @@ -95,6 +38,7 @@ func NewGitRepo(path string, clockLoaders []ClockLoader) (*GitRepo, error) { } repo := &GitRepo{ + gitCli: gitCli{path: path}, path: path, clocks: make(map[string]lamport.Clock), keyring: k, @@ -112,6 +56,7 @@ func NewGitRepo(path string, clockLoaders []ClockLoader) (*GitRepo, error) { // Fix the path to be sure we are at the root repo.path = stdout + repo.gitCli.path = stdout for _, loader := range clockLoaders { allExist := true @@ -135,6 +80,7 @@ func NewGitRepo(path string, clockLoaders []ClockLoader) (*GitRepo, error) { // InitGitRepo create a new empty git repo at the given path func InitGitRepo(path string) (*GitRepo, error) { repo := &GitRepo{ + gitCli: gitCli{path: path}, path: path + "/.git", clocks: make(map[string]lamport.Clock), } @@ -150,6 +96,7 @@ func InitGitRepo(path string) (*GitRepo, error) { // InitBareGitRepo create a new --bare empty git repo at the given path func InitBareGitRepo(path string) (*GitRepo, error) { repo := &GitRepo{ + gitCli: gitCli{path: path}, path: path, clocks: make(map[string]lamport.Clock), } @@ -162,6 +109,26 @@ func InitBareGitRepo(path string) (*GitRepo, error) { return repo, nil } +// LocalConfig give access to the repository scoped configuration +func (repo *GitRepo) LocalConfig() Config { + return newGitConfig(repo.gitCli, false) +} + +// GlobalConfig give access to the global scoped configuration +func (repo *GitRepo) GlobalConfig() Config { + return newGitConfig(repo.gitCli, true) +} + +// AnyConfig give access to a merged local/global configuration +func (repo *GitRepo) AnyConfig() ConfigRead { + return mergeConfig(repo.LocalConfig(), repo.GlobalConfig()) +} + +// Keyring give access to a user-wide storage for secrets +func (repo *GitRepo) Keyring() Keyring { + return repo.keyring +} + // GetPath returns the path to the repo. func (repo *GitRepo) GetPath() string { return repo.path diff --git a/repository/git_cli.go b/repository/git_cli.go new file mode 100644 index 00000000..085b1cda --- /dev/null +++ b/repository/git_cli.go @@ -0,0 +1,56 @@ +package repository + +import ( + "bytes" + "fmt" + "io" + "os/exec" + "strings" +) + +// gitCli is a helper to launch CLI git commands +type gitCli struct { + path string +} + +// Run the given git command with the given I/O reader/writers, returning an error if it fails. +func (cli gitCli) runGitCommandWithIO(stdin io.Reader, stdout, stderr io.Writer, args ...string) error { + // make sure that the working directory for the command + // always exist, in particular when running "git init". + path := strings.TrimSuffix(cli.path, ".git") + + // fmt.Printf("[%s] Running git %s\n", path, strings.Join(args, " ")) + + cmd := exec.Command("git", args...) + cmd.Dir = path + cmd.Stdin = stdin + cmd.Stdout = stdout + cmd.Stderr = stderr + + return cmd.Run() +} + +// Run the given git command and return its stdout, or an error if the command fails. +func (cli gitCli) runGitCommandRaw(stdin io.Reader, args ...string) (string, string, error) { + var stdout bytes.Buffer + var stderr bytes.Buffer + err := cli.runGitCommandWithIO(stdin, &stdout, &stderr, args...) + return strings.TrimSpace(stdout.String()), strings.TrimSpace(stderr.String()), err +} + +// Run the given git command and return its stdout, or an error if the command fails. +func (cli gitCli) runGitCommandWithStdin(stdin io.Reader, args ...string) (string, error) { + stdout, stderr, err := cli.runGitCommandRaw(stdin, args...) + if err != nil { + if stderr == "" { + stderr = "Error running git command: " + strings.Join(args, " ") + } + err = fmt.Errorf(stderr) + } + return stdout, err +} + +// Run the given git command and return its stdout, or an error if the command fails. +func (cli gitCli) runGitCommand(args ...string) (string, error) { + return cli.runGitCommandWithStdin(nil, args...) +} diff --git a/repository/git_config.go b/repository/git_config.go index 987cf195..b46cc69b 100644 --- a/repository/git_config.go +++ b/repository/git_config.go @@ -14,24 +14,24 @@ import ( var _ Config = &gitConfig{} type gitConfig struct { - repo *GitRepo + cli gitCli localityFlag string } -func newGitConfig(repo *GitRepo, global bool) *gitConfig { +func newGitConfig(cli gitCli, global bool) *gitConfig { localityFlag := "--local" if global { localityFlag = "--global" } return &gitConfig{ - repo: repo, + cli: cli, localityFlag: localityFlag, } } // StoreString store a single key/value pair in the config of the repo func (gc *gitConfig) StoreString(key string, value string) error { - _, err := gc.repo.runGitCommand("config", gc.localityFlag, "--replace-all", key, value) + _, err := gc.cli.runGitCommand("config", gc.localityFlag, "--replace-all", key, value) return err } @@ -45,7 +45,7 @@ func (gc *gitConfig) StoreTimestamp(key string, value time.Time) error { // ReadAll read all key/value pair matching the key prefix func (gc *gitConfig) ReadAll(keyPrefix string) (map[string]string, error) { - stdout, err := gc.repo.runGitCommand("config", gc.localityFlag, "--includes", "--get-regexp", keyPrefix) + stdout, err := gc.cli.runGitCommand("config", gc.localityFlag, "--includes", "--get-regexp", keyPrefix) // / \ // / ! \ @@ -74,7 +74,7 @@ func (gc *gitConfig) ReadAll(keyPrefix string) (map[string]string, error) { } func (gc *gitConfig) ReadString(key string) (string, error) { - stdout, err := gc.repo.runGitCommand("config", gc.localityFlag, "--includes", "--get-all", key) + stdout, err := gc.cli.runGitCommand("config", gc.localityFlag, "--includes", "--get-all", key) // / \ // / ! \ @@ -116,12 +116,12 @@ func (gc *gitConfig) ReadTimestamp(key string) (time.Time, error) { } func (gc *gitConfig) rmSection(keyPrefix string) error { - _, err := gc.repo.runGitCommand("config", gc.localityFlag, "--remove-section", keyPrefix) + _, err := gc.cli.runGitCommand("config", gc.localityFlag, "--remove-section", keyPrefix) return err } func (gc *gitConfig) unsetAll(keyPrefix string) error { - _, err := gc.repo.runGitCommand("config", gc.localityFlag, "--unset-all", keyPrefix) + _, err := gc.cli.runGitCommand("config", gc.localityFlag, "--unset-all", keyPrefix) return err } @@ -180,7 +180,7 @@ func (gc *gitConfig) RemoveAll(keyPrefix string) error { } func (gc *gitConfig) gitVersion() (*semver.Version, error) { - versionOut, err := gc.repo.runGitCommand("version") + versionOut, err := gc.cli.runGitCommand("version") if err != nil { return nil, err } diff --git a/repository/gogit.go b/repository/gogit.go index b0a4672b..f248235c 100644 --- a/repository/gogit.go +++ b/repository/gogit.go @@ -158,14 +158,25 @@ func InitBareGoGitRepo(path string) (*GoGitRepo, error) { }, nil } +// LocalConfig give access to the repository scoped configuration func (repo *GoGitRepo) LocalConfig() Config { return newGoGitConfig(repo.r) } +// GlobalConfig give access to the global scoped configuration func (repo *GoGitRepo) GlobalConfig() Config { - panic("go-git doesn't support writing global config") + // TODO: replace that with go-git native implementation once it's supported + // see: https://github.com/go-git/go-git + // see: https://github.com/src-d/go-git/issues/760 + return newGitConfig(gitCli{repo.path}, true) } +// AnyConfig give access to a merged local/global configuration +func (repo *GoGitRepo) AnyConfig() ConfigRead { + return mergeConfig(repo.LocalConfig(), repo.GlobalConfig()) +} + +// Keyring give access to a user-wide storage for secrets func (repo *GoGitRepo) Keyring() Keyring { return repo.keyring } @@ -288,6 +299,7 @@ func (repo *GoGitRepo) ReadData(hash Hash) ([]byte, error) { return ioutil.ReadAll(r) } +// StoreTree will store a mapping key-->Hash as a Git tree func (repo *GoGitRepo) StoreTree(mapping []TreeEntry) (Hash, error) { var tree object.Tree @@ -319,6 +331,7 @@ func (repo *GoGitRepo) StoreTree(mapping []TreeEntry) (Hash, error) { return Hash(hash.String()), nil } +// ReadTree will return the list of entries in a Git tree func (repo *GoGitRepo) ReadTree(hash Hash) ([]TreeEntry, error) { obj, err := repo.r.TreeObject(plumbing.NewHash(hash.String())) if err != nil { @@ -342,10 +355,12 @@ func (repo *GoGitRepo) ReadTree(hash Hash) ([]TreeEntry, error) { return treeEntries, nil } +// StoreCommit will store a Git commit with the given Git tree func (repo *GoGitRepo) StoreCommit(treeHash Hash) (Hash, error) { return repo.StoreCommitWithParent(treeHash, "") } +// StoreCommit will store a Git commit with the given Git tree func (repo *GoGitRepo) StoreCommitWithParent(treeHash Hash, parent Hash) (Hash, error) { cfg, err := repo.r.Config() if err != nil { @@ -386,6 +401,7 @@ func (repo *GoGitRepo) StoreCommitWithParent(treeHash Hash, parent Hash) (Hash, return Hash(hash.String()), nil } +// GetTreeHash return the git tree hash referenced in a commit func (repo *GoGitRepo) GetTreeHash(commit Hash) (Hash, error) { obj, err := repo.r.CommitObject(plumbing.NewHash(commit.String())) if err != nil { @@ -395,6 +411,7 @@ func (repo *GoGitRepo) GetTreeHash(commit Hash) (Hash, error) { return Hash(obj.TreeHash.String()), nil } +// FindCommonAncestor will return the last common ancestor of two chain of commit func (repo *GoGitRepo) FindCommonAncestor(commit1 Hash, commit2 Hash) (Hash, error) { obj1, err := repo.r.CommitObject(plumbing.NewHash(commit1.String())) if err != nil { @@ -413,14 +430,17 @@ func (repo *GoGitRepo) FindCommonAncestor(commit1 Hash, commit2 Hash) (Hash, err return Hash(commits[0].Hash.String()), nil } +// UpdateRef will create or update a Git reference func (repo *GoGitRepo) UpdateRef(ref string, hash Hash) error { return repo.r.Storer.SetReference(plumbing.NewHashReference(plumbing.ReferenceName(ref), plumbing.NewHash(hash.String()))) } +// RemoveRef will remove a Git reference func (repo *GoGitRepo) RemoveRef(ref string) error { return repo.r.Storer.RemoveReference(plumbing.ReferenceName(ref)) } +// ListRefs will return a list of Git ref matching the given refspec func (repo *GoGitRepo) ListRefs(refPrefix string) ([]string, error) { refIter, err := repo.r.References() if err != nil { @@ -442,6 +462,7 @@ func (repo *GoGitRepo) ListRefs(refPrefix string) ([]string, error) { return refs, nil } +// RefExist will check if a reference exist in Git func (repo *GoGitRepo) RefExist(ref string) (bool, error) { _, err := repo.r.Reference(plumbing.ReferenceName(ref), false) if err == nil { @@ -452,6 +473,7 @@ func (repo *GoGitRepo) RefExist(ref string) (bool, error) { return false, err } +// CopyRef will create a new reference with the same value as another one func (repo *GoGitRepo) CopyRef(source string, dest string) error { r, err := repo.r.Reference(plumbing.ReferenceName(source), false) if err != nil { @@ -460,6 +482,7 @@ func (repo *GoGitRepo) CopyRef(source string, dest string) error { return repo.r.Storer.SetReference(plumbing.NewHashReference(plumbing.ReferenceName(dest), r.Hash())) } +// ListCommits will return the list of tree hashes of a ref, in chronological order func (repo *GoGitRepo) ListCommits(ref string) ([]Hash, error) { r, err := repo.r.Reference(plumbing.ReferenceName(ref), false) if err != nil { diff --git a/repository/mock_repo.go b/repository/mock_repo.go index 07425765..628939aa 100644 --- a/repository/mock_repo.go +++ b/repository/mock_repo.go @@ -35,20 +35,20 @@ func NewMockRepoForTest() *mockRepoForTest { var _ RepoConfig = &mockRepoConfig{} type mockRepoConfig struct { - config *MemConfig + localConfig *MemConfig globalConfig *MemConfig } func NewMockRepoConfig() *mockRepoConfig { return &mockRepoConfig{ - config: NewMemConfig(), + localConfig: NewMemConfig(), globalConfig: NewMemConfig(), } } // LocalConfig give access to the repository scoped configuration func (r *mockRepoConfig) LocalConfig() Config { - return r.config + return r.localConfig } // GlobalConfig give access to the git global configuration @@ -56,6 +56,11 @@ func (r *mockRepoConfig) GlobalConfig() Config { return r.globalConfig } +// AnyConfig give access to a merged local/global configuration +func (r *mockRepoConfig) AnyConfig() ConfigRead { + return mergeConfig(r.localConfig, r.globalConfig) +} + var _ RepoKeyring = &mockRepoKeyring{} type mockRepoKeyring struct { diff --git a/repository/repo.go b/repository/repo.go index 6349007b..2eb27e82 100644 --- a/repository/repo.go +++ b/repository/repo.go @@ -32,6 +32,12 @@ type ClockedRepo interface { type RepoConfig interface { // LocalConfig give access to the repository scoped configuration LocalConfig() Config + + // GlobalConfig give access to the global scoped configuration + GlobalConfig() Config + + // AnyConfig give access to a merged local/global configuration + AnyConfig() ConfigRead } // RepoKeyring give access to a user-wide storage for secrets diff --git a/repository/repo_testing.go b/repository/repo_testing.go index 53375a5b..cfa26631 100644 --- a/repository/repo_testing.go +++ b/repository/repo_testing.go @@ -53,162 +53,171 @@ func RepoTest(t *testing.T, creator RepoCreator, cleaner RepoCleaner) { true: "Bare", } { t.Run(name, func(t *testing.T) { - t.Run("Blob-Tree-Commit-Ref", func(t *testing.T) { - repo := creator(bare) - defer cleaner(repo) + repo := creator(bare) + defer cleaner(repo) - // Blob - - data := randomData() - - blobHash1, err := repo.StoreData(data) - require.NoError(t, err) - require.True(t, blobHash1.IsValid()) - - blob1Read, err := repo.ReadData(blobHash1) - require.NoError(t, err) - require.Equal(t, data, blob1Read) - - // Tree - - blobHash2, err := repo.StoreData(randomData()) - require.NoError(t, err) - blobHash3, err := repo.StoreData(randomData()) - require.NoError(t, err) - - tree1 := []TreeEntry{ - { - ObjectType: Blob, - Hash: blobHash1, - Name: "blob1", - }, - { - ObjectType: Blob, - Hash: blobHash2, - Name: "blob2", - }, - } - - treeHash1, err := repo.StoreTree(tree1) - require.NoError(t, err) - require.True(t, treeHash1.IsValid()) - - tree1Read, err := repo.ReadTree(treeHash1) - require.NoError(t, err) - require.ElementsMatch(t, tree1, tree1Read) - - tree2 := []TreeEntry{ - { - ObjectType: Tree, - Hash: treeHash1, - Name: "tree1", - }, - { - ObjectType: Blob, - Hash: blobHash3, - Name: "blob3", - }, - } - - treeHash2, err := repo.StoreTree(tree2) - require.NoError(t, err) - require.True(t, treeHash2.IsValid()) - - tree2Read, err := repo.ReadTree(treeHash2) - require.NoError(t, err) - require.ElementsMatch(t, tree2, tree2Read) - - // Commit - - commit1, err := repo.StoreCommit(treeHash1) - require.NoError(t, err) - require.True(t, commit1.IsValid()) - - treeHash1Read, err := repo.GetTreeHash(commit1) - require.NoError(t, err) - require.Equal(t, treeHash1, treeHash1Read) - - commit2, err := repo.StoreCommitWithParent(treeHash2, commit1) - require.NoError(t, err) - require.True(t, commit2.IsValid()) - - treeHash2Read, err := repo.GetTreeHash(commit2) - require.NoError(t, err) - require.Equal(t, treeHash2, treeHash2Read) - - // Ref - - exist1, err := repo.RefExist("refs/bugs/ref1") - require.NoError(t, err) - require.False(t, exist1) - - err = repo.UpdateRef("refs/bugs/ref1", commit2) - require.NoError(t, err) - - exist1, err = repo.RefExist("refs/bugs/ref1") - require.NoError(t, err) - require.True(t, exist1) - - ls, err := repo.ListRefs("refs/bugs") - require.NoError(t, err) - require.ElementsMatch(t, []string{"refs/bugs/ref1"}, ls) - - err = repo.CopyRef("refs/bugs/ref1", "refs/bugs/ref2") - require.NoError(t, err) - - ls, err = repo.ListRefs("refs/bugs") - require.NoError(t, err) - require.ElementsMatch(t, []string{"refs/bugs/ref1", "refs/bugs/ref2"}, ls) - - commits, err := repo.ListCommits("refs/bugs/ref2") - require.NoError(t, err) - require.ElementsMatch(t, []Hash{commit1, commit2}, commits) - - // Graph - - commit3, err := repo.StoreCommitWithParent(treeHash1, commit1) - require.NoError(t, err) - - ancestorHash, err := repo.FindCommonAncestor(commit2, commit3) - require.NoError(t, err) - require.Equal(t, commit1, ancestorHash) - - err = repo.RemoveRef("refs/bugs/ref1") - require.NoError(t, err) + t.Run("Data", func(t *testing.T) { + RepoDataTest(t, repo) }) - t.Run("Local config", func(t *testing.T) { - repo := creator(bare) - defer cleaner(repo) - - testConfig(t, repo.LocalConfig()) + t.Run("Config", func(t *testing.T) { + RepoConfigTest(t, repo) }) t.Run("Clocks", func(t *testing.T) { - repo := creator(bare) - defer cleaner(repo) - - clock, err := repo.GetOrCreateClock("foo") - require.NoError(t, err) - require.Equal(t, lamport.Time(1), clock.Time()) - - time, err := clock.Increment() - require.NoError(t, err) - require.Equal(t, lamport.Time(1), time) - require.Equal(t, lamport.Time(2), clock.Time()) - - clock2, err := repo.GetOrCreateClock("foo") - require.NoError(t, err) - require.Equal(t, lamport.Time(2), clock2.Time()) - - clock3, err := repo.GetOrCreateClock("bar") - require.NoError(t, err) - require.Equal(t, lamport.Time(1), clock3.Time()) + RepoClockTest(t, repo) }) }) } } +// helper to test a RepoConfig +func RepoConfigTest(t *testing.T, repo RepoConfig) { + testConfig(t, repo.LocalConfig()) +} + +// helper to test a RepoData +func RepoDataTest(t *testing.T, repo RepoData) { + // Blob + + data := randomData() + + blobHash1, err := repo.StoreData(data) + require.NoError(t, err) + require.True(t, blobHash1.IsValid()) + + blob1Read, err := repo.ReadData(blobHash1) + require.NoError(t, err) + require.Equal(t, data, blob1Read) + + // Tree + + blobHash2, err := repo.StoreData(randomData()) + require.NoError(t, err) + blobHash3, err := repo.StoreData(randomData()) + require.NoError(t, err) + + tree1 := []TreeEntry{ + { + ObjectType: Blob, + Hash: blobHash1, + Name: "blob1", + }, + { + ObjectType: Blob, + Hash: blobHash2, + Name: "blob2", + }, + } + + treeHash1, err := repo.StoreTree(tree1) + require.NoError(t, err) + require.True(t, treeHash1.IsValid()) + + tree1Read, err := repo.ReadTree(treeHash1) + require.NoError(t, err) + require.ElementsMatch(t, tree1, tree1Read) + + tree2 := []TreeEntry{ + { + ObjectType: Tree, + Hash: treeHash1, + Name: "tree1", + }, + { + ObjectType: Blob, + Hash: blobHash3, + Name: "blob3", + }, + } + + treeHash2, err := repo.StoreTree(tree2) + require.NoError(t, err) + require.True(t, treeHash2.IsValid()) + + tree2Read, err := repo.ReadTree(treeHash2) + require.NoError(t, err) + require.ElementsMatch(t, tree2, tree2Read) + + // Commit + + commit1, err := repo.StoreCommit(treeHash1) + require.NoError(t, err) + require.True(t, commit1.IsValid()) + + treeHash1Read, err := repo.GetTreeHash(commit1) + require.NoError(t, err) + require.Equal(t, treeHash1, treeHash1Read) + + commit2, err := repo.StoreCommitWithParent(treeHash2, commit1) + require.NoError(t, err) + require.True(t, commit2.IsValid()) + + treeHash2Read, err := repo.GetTreeHash(commit2) + require.NoError(t, err) + require.Equal(t, treeHash2, treeHash2Read) + + // Ref + + exist1, err := repo.RefExist("refs/bugs/ref1") + require.NoError(t, err) + require.False(t, exist1) + + err = repo.UpdateRef("refs/bugs/ref1", commit2) + require.NoError(t, err) + + exist1, err = repo.RefExist("refs/bugs/ref1") + require.NoError(t, err) + require.True(t, exist1) + + ls, err := repo.ListRefs("refs/bugs") + require.NoError(t, err) + require.ElementsMatch(t, []string{"refs/bugs/ref1"}, ls) + + err = repo.CopyRef("refs/bugs/ref1", "refs/bugs/ref2") + require.NoError(t, err) + + ls, err = repo.ListRefs("refs/bugs") + require.NoError(t, err) + require.ElementsMatch(t, []string{"refs/bugs/ref1", "refs/bugs/ref2"}, ls) + + commits, err := repo.ListCommits("refs/bugs/ref2") + require.NoError(t, err) + require.ElementsMatch(t, []Hash{commit1, commit2}, commits) + + // Graph + + commit3, err := repo.StoreCommitWithParent(treeHash1, commit1) + require.NoError(t, err) + + ancestorHash, err := repo.FindCommonAncestor(commit2, commit3) + require.NoError(t, err) + require.Equal(t, commit1, ancestorHash) + + err = repo.RemoveRef("refs/bugs/ref1") + require.NoError(t, err) +} + +// helper to test a RepoClock +func RepoClockTest(t *testing.T, repo RepoClock) { + clock, err := repo.GetOrCreateClock("foo") + require.NoError(t, err) + require.Equal(t, lamport.Time(1), clock.Time()) + + time, err := clock.Increment() + require.NoError(t, err) + require.Equal(t, lamport.Time(1), time) + require.Equal(t, lamport.Time(2), clock.Time()) + + clock2, err := repo.GetOrCreateClock("foo") + require.NoError(t, err) + require.Equal(t, lamport.Time(2), clock2.Time()) + + clock3, err := repo.GetOrCreateClock("bar") + require.NoError(t, err) + require.Equal(t, lamport.Time(1), clock3.Time()) +} + func randomData() []byte { var letterRunes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" b := make([]byte, 32) From 4f172432b1fb983c57aa258a93e24cbb36c8e1fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sun, 27 Sep 2020 19:25:37 +0200 Subject: [PATCH 19/25] repo: fix manu bugs in go-git config --- repository/config_testing.go | 97 ++++++++++++++++++++++++++++-------- repository/gogit_config.go | 90 +++++++++++++++++++++------------ 2 files changed, 133 insertions(+), 54 deletions(-) diff --git a/repository/config_testing.go b/repository/config_testing.go index 25639d59..445f8721 100644 --- a/repository/config_testing.go +++ b/repository/config_testing.go @@ -2,62 +2,115 @@ package repository import ( "testing" + "time" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func testConfig(t *testing.T, config Config) { + // string err := config.StoreString("section.key", "value") - assert.NoError(t, err) + require.NoError(t, err) val, err := config.ReadString("section.key") - assert.NoError(t, err) - assert.Equal(t, "value", val) + require.NoError(t, err) + require.Equal(t, "value", val) - err = config.StoreString("section.true", "true") - assert.NoError(t, err) + // bool + err = config.StoreBool("section.true", true) + require.NoError(t, err) val2, err := config.ReadBool("section.true") - assert.NoError(t, err) - assert.Equal(t, true, val2) + require.NoError(t, err) + require.Equal(t, true, val2) + // timestamp + err = config.StoreTimestamp("section.time", time.Unix(1234, 0)) + require.NoError(t, err) + + val3, err := config.ReadTimestamp("section.time") + require.NoError(t, err) + require.Equal(t, time.Unix(1234, 0), val3) + + // ReadAll configs, err := config.ReadAll("section") - assert.NoError(t, err) - assert.Equal(t, map[string]string{ + require.NoError(t, err) + require.Equal(t, map[string]string{ "section.key": "value", "section.true": "true", + "section.time": "1234", }, configs) + // RemoveAll err = config.RemoveAll("section.true") - assert.NoError(t, err) + require.NoError(t, err) configs, err = config.ReadAll("section") - assert.NoError(t, err) - assert.Equal(t, map[string]string{ - "section.key": "value", + require.NoError(t, err) + require.Equal(t, map[string]string{ + "section.key": "value", + "section.time": "1234", }, configs) _, err = config.ReadBool("section.true") - assert.Equal(t, ErrNoConfigEntry, err) + require.Equal(t, ErrNoConfigEntry, err) err = config.RemoveAll("section.nonexistingkey") - assert.Error(t, err) + require.Error(t, err) err = config.RemoveAll("section.key") - assert.NoError(t, err) + require.NoError(t, err) _, err = config.ReadString("section.key") - assert.Equal(t, ErrNoConfigEntry, err) + require.Equal(t, ErrNoConfigEntry, err) err = config.RemoveAll("nonexistingsection") - assert.Error(t, err) + require.Error(t, err) + + err = config.RemoveAll("section.time") + require.NoError(t, err) err = config.RemoveAll("section") - assert.Error(t, err) + require.Error(t, err) _, err = config.ReadString("section.key") - assert.Error(t, err) + require.Error(t, err) err = config.RemoveAll("section.key") - assert.Error(t, err) + require.Error(t, err) + + // section + subsections + require.NoError(t, config.StoreString("section.opt1", "foo")) + require.NoError(t, config.StoreString("section.opt2", "foo2")) + require.NoError(t, config.StoreString("section.subsection.opt1", "foo3")) + require.NoError(t, config.StoreString("section.subsection.opt2", "foo4")) + require.NoError(t, config.StoreString("section.subsection.subsection.opt1", "foo5")) + require.NoError(t, config.StoreString("section.subsection.subsection.opt2", "foo6")) + + all, err := config.ReadAll("section") + require.NoError(t, err) + require.Equal(t, map[string]string{ + "section.opt1": "foo", + "section.opt2": "foo2", + "section.subsection.opt1": "foo3", + "section.subsection.opt2": "foo4", + "section.subsection.subsection.opt1": "foo5", + "section.subsection.subsection.opt2": "foo6", + }, all) + + all, err = config.ReadAll("section.subsection") + require.NoError(t, err) + require.Equal(t, map[string]string{ + "section.subsection.opt1": "foo3", + "section.subsection.opt2": "foo4", + "section.subsection.subsection.opt1": "foo5", + "section.subsection.subsection.opt2": "foo6", + }, all) + + all, err = config.ReadAll("section.subsection.subsection") + require.NoError(t, err) + require.Equal(t, map[string]string{ + "section.subsection.subsection.opt1": "foo5", + "section.subsection.subsection.opt2": "foo6", + }, all) } diff --git a/repository/gogit_config.go b/repository/gogit_config.go index 000658a8..80b0a215 100644 --- a/repository/gogit_config.go +++ b/repository/gogit_config.go @@ -7,7 +7,6 @@ import ( "time" gogit "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/plumbing/format/config" ) var _ Config = &goGitConfig{} @@ -35,7 +34,7 @@ func (ggc *goGitConfig) StoreString(key, value string) error { cfg.Raw.Section(split[0]).SetOption(split[1], value) default: section := split[0] - subsection := strings.Join(split[1:len(split)-2], ".") + subsection := strings.Join(split[1:len(split)-1], ".") option := split[len(split)-1] cfg.Raw.Section(section).Subsection(subsection).SetOption(option, value) } @@ -58,33 +57,52 @@ func (ggc *goGitConfig) ReadAll(keyPrefix string) (map[string]string, error) { } split := strings.Split(keyPrefix, ".") - - var opts config.Options + result := make(map[string]string) switch { - case len(split) < 1: - return nil, fmt.Errorf("invalid key prefix") + case keyPrefix == "": + for _, section := range cfg.Raw.Sections { + for _, option := range section.Options { + result[fmt.Sprintf("%s.%s", section.Name, option.Key)] = option.Value + } + for _, subsection := range section.Subsections { + for _, option := range subsection.Options { + result[fmt.Sprintf("%s.%s.%s", section.Name, subsection.Name, option.Key)] = option.Value + } + } + } case len(split) == 1: - opts = cfg.Raw.Section(split[0]).Options + if !cfg.Raw.HasSection(split[0]) { + return nil, fmt.Errorf("invalid section") + } + section := cfg.Raw.Section(split[0]) + for _, option := range section.Options { + result[fmt.Sprintf("%s.%s", section.Name, option.Key)] = option.Value + } + for _, subsection := range section.Subsections { + for _, option := range subsection.Options { + result[fmt.Sprintf("%s.%s.%s", section.Name, subsection.Name, option.Key)] = option.Value + } + } default: - section := split[0] - subsection := strings.Join(split[1:len(split)-1], ".") - opts = cfg.Raw.Section(section).Subsection(subsection).Options + if !cfg.Raw.HasSection(split[0]) { + return nil, fmt.Errorf("invalid section") + } + section := cfg.Raw.Section(split[0]) + rest := strings.Join(split[1:], ".") + for _, subsection := range section.Subsections { + if strings.HasPrefix(subsection.Name, rest) { + for _, option := range subsection.Options { + result[fmt.Sprintf("%s.%s.%s", section.Name, subsection.Name, option.Key)] = option.Value + } + } + } } - if len(opts) == 0 { + if len(result) == 0 { return nil, fmt.Errorf("invalid section") } - if keyPrefix[len(keyPrefix)-1:] != "." { - keyPrefix += "." - } - - result := make(map[string]string, len(opts)) - for _, opt := range opts { - result[keyPrefix+opt.Key] = opt.Value - } - return result, nil } @@ -159,26 +177,34 @@ func (ggc *goGitConfig) RemoveAll(keyPrefix string) error { split := strings.Split(keyPrefix, ".") switch { - case len(split) < 1: - return fmt.Errorf("invalid key prefix") + case keyPrefix == "": + cfg.Raw.Sections = nil + // warning: this does not actually remove everything as go-git config hold + // some entries in multiple places (cfg.User ...) case len(split) == 1: - if len(cfg.Raw.Section(split[0]).Options) > 0 { + if cfg.Raw.HasSection(split[0]) { cfg.Raw.RemoveSection(split[0]) } else { return fmt.Errorf("invalid key prefix") } default: - section := split[0] + if !cfg.Raw.HasSection(split[0]) { + return fmt.Errorf("invalid key prefix") + } + section := cfg.Raw.Section(split[0]) rest := strings.Join(split[1:], ".") - if cfg.Raw.Section(section).HasSubsection(rest) { - cfg.Raw.RemoveSubsection(section, rest) - } else { - if cfg.Raw.Section(section).HasOption(rest) { - cfg.Raw.Section(section).RemoveOption(rest) - } else { - return fmt.Errorf("invalid key prefix") - } + ok := false + if section.HasSubsection(rest) { + section.RemoveSubsection(rest) + ok = true + } + if section.HasOption(rest) { + section.RemoveOption(rest) + ok = true + } + if !ok { + return fmt.Errorf("invalid key prefix") } } From eb88f0e4463ea1aef5494314fa2a9607aaa262dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sun, 27 Sep 2020 20:31:09 +0200 Subject: [PATCH 20/25] repo: more config related bug fixes --- identity/identity_user.go | 29 +++++----- repository/config.go | 20 +++++++ repository/gogit.go | 4 +- repository/gogit_config.go | 115 ++++++++++++++++++++++--------------- 4 files changed, 104 insertions(+), 64 deletions(-) diff --git a/identity/identity_user.go b/identity/identity_user.go index 60622c12..cd67459e 100644 --- a/identity/identity_user.go +++ b/identity/identity_user.go @@ -35,23 +35,18 @@ func GetUserIdentity(repo repository.Repo) (*Identity, error) { } func GetUserIdentityId(repo repository.Repo) (entity.Id, error) { - configs, err := repo.LocalConfig().ReadAll(identityConfigKey) + val, err := repo.LocalConfig().ReadString(identityConfigKey) + if err == repository.ErrNoConfigEntry { + return entity.UnsetId, ErrNoIdentitySet + } + if err == repository.ErrMultipleConfigEntry { + return entity.UnsetId, ErrMultipleIdentitiesSet + } if err != nil { return entity.UnsetId, err } - if len(configs) == 0 { - return entity.UnsetId, ErrNoIdentitySet - } - - if len(configs) > 1 { - return entity.UnsetId, ErrMultipleIdentitiesSet - } - - var id entity.Id - for _, val := range configs { - id = entity.Id(val) - } + var id = entity.Id(val) if err := id.Validate(); err != nil { return entity.UnsetId, err @@ -62,10 +57,12 @@ func GetUserIdentityId(repo repository.Repo) (entity.Id, error) { // IsUserIdentitySet say if the user has set his identity func IsUserIdentitySet(repo repository.Repo) (bool, error) { - configs, err := repo.LocalConfig().ReadAll(identityConfigKey) + _, err := repo.LocalConfig().ReadString(identityConfigKey) + if err == repository.ErrNoConfigEntry { + return false, nil + } if err != nil { return false, err } - - return len(configs) == 1, nil + return true, nil } diff --git a/repository/config.go b/repository/config.go index 4ea326b5..4db8d4be 100644 --- a/repository/config.go +++ b/repository/config.go @@ -123,3 +123,23 @@ func (m *mergedConfig) ReadTimestamp(key string) (time.Time, error) { } return m.global.ReadTimestamp(key) } + +var _ ConfigWrite = &configPanicWriter{} + +type configPanicWriter struct{} + +func (c configPanicWriter) StoreString(key, value string) error { + panic("not implemented") +} + +func (c configPanicWriter) StoreTimestamp(key string, value time.Time) error { + panic("not implemented") +} + +func (c configPanicWriter) StoreBool(key string, value bool) error { + panic("not implemented") +} + +func (c configPanicWriter) RemoveAll(keyPrefix string) error { + panic("not implemented") +} diff --git a/repository/gogit.go b/repository/gogit.go index f248235c..684f8a72 100644 --- a/repository/gogit.go +++ b/repository/gogit.go @@ -160,7 +160,7 @@ func InitBareGoGitRepo(path string) (*GoGitRepo, error) { // LocalConfig give access to the repository scoped configuration func (repo *GoGitRepo) LocalConfig() Config { - return newGoGitConfig(repo.r) + return newGoGitLocalConfig(repo.r) } // GlobalConfig give access to the global scoped configuration @@ -168,7 +168,7 @@ func (repo *GoGitRepo) GlobalConfig() Config { // TODO: replace that with go-git native implementation once it's supported // see: https://github.com/go-git/go-git // see: https://github.com/src-d/go-git/issues/760 - return newGitConfig(gitCli{repo.path}, true) + return newGoGitGlobalConfig(repo.r) } // AnyConfig give access to a merged local/global configuration diff --git a/repository/gogit_config.go b/repository/gogit_config.go index 80b0a215..7812de76 100644 --- a/repository/gogit_config.go +++ b/repository/gogit_config.go @@ -7,51 +7,40 @@ import ( "time" gogit "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/config" ) var _ Config = &goGitConfig{} type goGitConfig struct { - repo *gogit.Repository + ConfigRead + ConfigWrite } -func newGoGitConfig(repo *gogit.Repository) *goGitConfig { - return &goGitConfig{repo: repo} -} - -func (ggc *goGitConfig) StoreString(key, value string) error { - cfg, err := ggc.repo.Config() - if err != nil { - return err +func newGoGitLocalConfig(repo *gogit.Repository) *goGitConfig { + return &goGitConfig{ + ConfigRead: &goGitConfigReader{getConfig: repo.Config}, + ConfigWrite: &goGitConfigWriter{repo: repo}, } +} - split := strings.Split(key, ".") - - switch { - case len(split) <= 1: - return fmt.Errorf("invalid key") - case len(split) == 2: - cfg.Raw.Section(split[0]).SetOption(split[1], value) - default: - section := split[0] - subsection := strings.Join(split[1:len(split)-1], ".") - option := split[len(split)-1] - cfg.Raw.Section(section).Subsection(subsection).SetOption(option, value) +func newGoGitGlobalConfig(repo *gogit.Repository) *goGitConfig { + return &goGitConfig{ + ConfigRead: &goGitConfigReader{getConfig: func() (*config.Config, error) { + return config.LoadConfig(config.GlobalScope) + }}, + ConfigWrite: &configPanicWriter{}, } - - return ggc.repo.SetConfig(cfg) } -func (ggc *goGitConfig) StoreTimestamp(key string, value time.Time) error { - return ggc.StoreString(key, strconv.Itoa(int(value.Unix()))) +var _ ConfigRead = &goGitConfigReader{} + +type goGitConfigReader struct { + getConfig func() (*config.Config, error) } -func (ggc *goGitConfig) StoreBool(key string, value bool) error { - return ggc.StoreString(key, strconv.FormatBool(value)) -} - -func (ggc *goGitConfig) ReadAll(keyPrefix string) (map[string]string, error) { - cfg, err := ggc.repo.Config() +func (cr *goGitConfigReader) ReadAll(keyPrefix string) (map[string]string, error) { + cfg, err := cr.getConfig() if err != nil { return nil, err } @@ -73,7 +62,7 @@ func (ggc *goGitConfig) ReadAll(keyPrefix string) (map[string]string, error) { } case len(split) == 1: if !cfg.Raw.HasSection(split[0]) { - return nil, fmt.Errorf("invalid section") + return nil, nil } section := cfg.Raw.Section(split[0]) for _, option := range section.Options { @@ -86,7 +75,7 @@ func (ggc *goGitConfig) ReadAll(keyPrefix string) (map[string]string, error) { } default: if !cfg.Raw.HasSection(split[0]) { - return nil, fmt.Errorf("invalid section") + return nil, nil } section := cfg.Raw.Section(split[0]) rest := strings.Join(split[1:], ".") @@ -99,15 +88,11 @@ func (ggc *goGitConfig) ReadAll(keyPrefix string) (map[string]string, error) { } } - if len(result) == 0 { - return nil, fmt.Errorf("invalid section") - } - return result, nil } -func (ggc *goGitConfig) ReadBool(key string) (bool, error) { - val, err := ggc.ReadString(key) +func (cr *goGitConfigReader) ReadBool(key string) (bool, error) { + val, err := cr.ReadString(key) if err != nil { return false, err } @@ -115,8 +100,8 @@ func (ggc *goGitConfig) ReadBool(key string) (bool, error) { return strconv.ParseBool(val) } -func (ggc *goGitConfig) ReadString(key string) (string, error) { - cfg, err := ggc.repo.Config() +func (cr *goGitConfigReader) ReadString(key string) (string, error) { + cfg, err := cr.getConfig() if err != nil { return "", err } @@ -160,16 +145,54 @@ func (ggc *goGitConfig) ReadString(key string) (string, error) { } } -func (ggc *goGitConfig) ReadTimestamp(key string) (time.Time, error) { - value, err := ggc.ReadString(key) +func (cr *goGitConfigReader) ReadTimestamp(key string) (time.Time, error) { + value, err := cr.ReadString(key) if err != nil { return time.Time{}, err } return ParseTimestamp(value) } -func (ggc *goGitConfig) RemoveAll(keyPrefix string) error { - cfg, err := ggc.repo.Config() +var _ ConfigWrite = &goGitConfigWriter{} + +// Only works for the local config as go-git only support that +type goGitConfigWriter struct { + repo *gogit.Repository +} + +func (cw *goGitConfigWriter) StoreString(key, value string) error { + cfg, err := cw.repo.Config() + if err != nil { + return err + } + + split := strings.Split(key, ".") + + switch { + case len(split) <= 1: + return fmt.Errorf("invalid key") + case len(split) == 2: + cfg.Raw.Section(split[0]).SetOption(split[1], value) + default: + section := split[0] + subsection := strings.Join(split[1:len(split)-1], ".") + option := split[len(split)-1] + cfg.Raw.Section(section).Subsection(subsection).SetOption(option, value) + } + + return cw.repo.SetConfig(cfg) +} + +func (cw *goGitConfigWriter) StoreTimestamp(key string, value time.Time) error { + return cw.StoreString(key, strconv.Itoa(int(value.Unix()))) +} + +func (cw *goGitConfigWriter) StoreBool(key string, value bool) error { + return cw.StoreString(key, strconv.FormatBool(value)) +} + +func (cw *goGitConfigWriter) RemoveAll(keyPrefix string) error { + cfg, err := cw.repo.Config() if err != nil { return err } @@ -208,5 +231,5 @@ func (ggc *goGitConfig) RemoveAll(keyPrefix string) error { } } - return ggc.repo.SetConfig(cfg) + return cw.repo.SetConfig(cfg) } From d8b49e025a01e45cfc90bb11b8a0645b616ec561 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sun, 27 Sep 2020 21:14:25 +0200 Subject: [PATCH 21/25] repo: ReadTree must accept either a commit or a tree hash --- repository/gogit.go | 27 ++++++++++++++++++++++++--- repository/repo.go | 1 + 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/repository/gogit.go b/repository/gogit.go index 684f8a72..6d3aede9 100644 --- a/repository/gogit.go +++ b/repository/gogit.go @@ -333,13 +333,34 @@ func (repo *GoGitRepo) StoreTree(mapping []TreeEntry) (Hash, error) { // ReadTree will return the list of entries in a Git tree func (repo *GoGitRepo) ReadTree(hash Hash) ([]TreeEntry, error) { - obj, err := repo.r.TreeObject(plumbing.NewHash(hash.String())) + h := plumbing.NewHash(hash.String()) + + // the given hash could be a tree or a commit + obj, err := repo.r.Storer.EncodedObject(plumbing.AnyObject, h) if err != nil { return nil, err } - treeEntries := make([]TreeEntry, len(obj.Entries)) - for i, entry := range obj.Entries { + var tree *object.Tree + switch obj.Type() { + case plumbing.TreeObject: + tree, err = object.DecodeTree(repo.r.Storer, obj) + case plumbing.CommitObject: + var commit *object.Commit + commit, err = object.DecodeCommit(repo.r.Storer, obj) + if err != nil { + return nil, err + } + tree, err = commit.Tree() + default: + return nil, fmt.Errorf("given hash is not a tree") + } + if err != nil { + return nil, err + } + + treeEntries := make([]TreeEntry, len(tree.Entries)) + for i, entry := range tree.Entries { objType := Blob if entry.Mode == filemode.Dir { objType = Tree diff --git a/repository/repo.go b/repository/repo.go index 2eb27e82..4b45a1c5 100644 --- a/repository/repo.go +++ b/repository/repo.go @@ -82,6 +82,7 @@ type RepoData interface { StoreTree(mapping []TreeEntry) (Hash, error) // ReadTree will return the list of entries in a Git tree + // The given hash could be from either a commit or a tree ReadTree(hash Hash) ([]TreeEntry, error) // StoreCommit will store a Git commit with the given Git tree From 736d0a2f0931d3e5466d9838839f5bbde606fe34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sun, 27 Sep 2020 22:56:53 +0200 Subject: [PATCH 22/25] repo: more testing for an edge case --- repository/repo_testing.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/repository/repo_testing.go b/repository/repo_testing.go index cfa26631..b1f47396 100644 --- a/repository/repo_testing.go +++ b/repository/repo_testing.go @@ -157,6 +157,11 @@ func RepoDataTest(t *testing.T, repo RepoData) { require.NoError(t, err) require.Equal(t, treeHash2, treeHash2Read) + // ReadTree should accept tree and commit hashes + tree1read, err := repo.ReadTree(commit1) + require.NoError(t, err) + require.Equal(t, tree1read, tree1) + // Ref exist1, err := repo.RefExist("refs/bugs/ref1") From 02146f0b404e3f247c2fcb540b10c42f37630735 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sun, 27 Sep 2020 22:59:32 +0200 Subject: [PATCH 23/25] use go-git by default! --- commands/env.go | 2 +- misc/random_bugs/cmd/main.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/commands/env.go b/commands/env.go index 59c5d33b..5658342d 100644 --- a/commands/env.go +++ b/commands/env.go @@ -54,7 +54,7 @@ func loadRepo(env *Env) func(*cobra.Command, []string) error { return fmt.Errorf("unable to get the current working directory: %q", err) } - env.repo, err = repository.NewGitRepo(cwd, []repository.ClockLoader{bug.ClockLoader}) + env.repo, err = repository.NewGoGitRepo(cwd, []repository.ClockLoader{bug.ClockLoader}) if err == repository.ErrNotARepo { return fmt.Errorf("%s must be run from within a git repo", rootCommandName) } diff --git a/misc/random_bugs/cmd/main.go b/misc/random_bugs/cmd/main.go index ec62b6ed..3127b4aa 100644 --- a/misc/random_bugs/cmd/main.go +++ b/misc/random_bugs/cmd/main.go @@ -20,7 +20,7 @@ func main() { bug.ClockLoader, } - repo, err := repository.NewGitRepo(dir, loaders) + repo, err := repository.NewGoGitRepo(dir, loaders) if err != nil { panic(err) } From 0acb3505ffe71718cb3b6b0e957cc921ea9ce880 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sun, 27 Sep 2020 23:14:51 +0200 Subject: [PATCH 24/25] repo: implement GetCoreEditor for go-git --- repository/gogit.go | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/repository/gogit.go b/repository/gogit.go index 6d3aede9..aac89d4b 100644 --- a/repository/gogit.go +++ b/repository/gogit.go @@ -208,8 +208,30 @@ func (repo *GoGitRepo) GetUserEmail() (string, error) { // GetCoreEditor returns the name of the editor that the user has used to configure git. func (repo *GoGitRepo) GetCoreEditor() (string, error) { + // See https://git-scm.com/docs/git-var + // The order of preference is the $GIT_EDITOR environment variable, then core.editor configuration, then $VISUAL, then $EDITOR, and then the default chosen at compile time, which is usually vi. - panic("implement me") + if val, ok := os.LookupEnv("GIT_EDITOR"); ok { + return val, nil + } + + val, err := repo.AnyConfig().ReadString("core.editor") + if err == nil && val != "" { + return val, nil + } + if err != nil && err != ErrNoConfigEntry { + return "", err + } + + if val, ok := os.LookupEnv("VISUAL"); ok { + return val, nil + } + + if val, ok := os.LookupEnv("EDITOR"); ok { + return val, nil + } + + return "vi", nil } // GetRemotes returns the configured remotes repositories. From 4055495c8ba983033459507f3032ca93c6ec006a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Tue, 29 Sep 2020 20:16:15 +0200 Subject: [PATCH 25/25] repo: fix wrong ordering in gogit's ListCommit --- repository/gogit.go | 14 ++++++-------- repository/repo_testing.go | 2 +- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/repository/gogit.go b/repository/gogit.go index aac89d4b..09f714ea 100644 --- a/repository/gogit.go +++ b/repository/gogit.go @@ -536,16 +536,14 @@ func (repo *GoGitRepo) ListCommits(ref string) ([]Hash, error) { if err != nil { return nil, err } - commits := []Hash{Hash(commit.Hash.String())} + hashes := []Hash{Hash(commit.Hash.String())} for { commit, err = commit.Parent(0) - + if err == object.ErrParentNotFound { + break + } if err != nil { - if err == object.ErrParentNotFound { - break - } - return nil, err } @@ -553,10 +551,10 @@ func (repo *GoGitRepo) ListCommits(ref string) ([]Hash, error) { return nil, fmt.Errorf("multiple parents") } - commits = append(commits, Hash(commit.Hash.String())) + hashes = append([]Hash{Hash(commit.Hash.String())}, hashes...) } - return commits, nil + return hashes, nil } // GetOrCreateClock return a Lamport clock stored in the Repo. diff --git a/repository/repo_testing.go b/repository/repo_testing.go index b1f47396..41b3609e 100644 --- a/repository/repo_testing.go +++ b/repository/repo_testing.go @@ -188,7 +188,7 @@ func RepoDataTest(t *testing.T, repo RepoData) { commits, err := repo.ListCommits("refs/bugs/ref2") require.NoError(t, err) - require.ElementsMatch(t, []Hash{commit1, commit2}, commits) + require.Equal(t, []Hash{commit1, commit2}, commits) // Graph