├── .envrc ├── .gitignore ├── Makefile ├── README.md ├── commands ├── commands.go ├── deploy │ └── deploy.go └── push │ └── push.go ├── default.nix ├── go.mod ├── go.sum ├── main.go └── util ├── cli_helpers.go ├── commands.go ├── git.go └── logger.go /.envrc: -------------------------------------------------------------------------------- 1 | use nix 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Go mod vendor 2 | vendor/ 3 | 4 | # Compiled binary 5 | pushnix 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION=$(shell git describe --always --dirty --tags) 2 | 3 | pushnix: 4 | go build -ldflags "-s -w -X main.Version=$(VERSION)" -o pushnix 5 | 6 | clean: 7 | rm -f pushnix 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pushnix 2 | 3 | A cli tool to push your NixOS configuration repository to a remote host using git and then run `nixos-rebuild` on the host using ssh. 4 | 5 | ## Setup 6 | 7 | 1. Make sure you have the following dependencies on the remote host. 8 | - `git-receive-pack` in path (included in `git` in nixpkgs). 9 | - A running SSH server and a remote user you can connect to. 10 | 2. On the remote NixOS host create a directory where you want the NixOS configuration to be. 11 | 3. Inside the directory setup the repository. 12 | - `git init` 13 | - `git config receive.denyCurrentBranch updateInstead` ([info](https://git-scm.com/docs/git-config#Documentation/git-config.txt-receivedenyCurrentBranch)) 14 | 4. In your local NixOS configuration git repository run `git remote add @:`. 15 | - `` - Whatever name you want this remote host to be called when interacting with it using `git` or `pushnix`. 16 | - `` - The remote user you can SSH to. 17 | - `` - DNS or IP of remote host you can SSH to. 18 | - `` - Path to git repository on the remote host. 19 | 20 | Now you've set it up so that you can run `git push ` and the git repository on the remote side will be updated (as long as it is checked out on the same branch as you are pushing). 21 | 22 | ## Usage 23 | 24 | Let's say you created a remote called `my_server` in your NixOS configuration git repository. 25 | 26 | Now you can run `pushnix deploy my_server` to push the configuration and rebuild the NixOS configuration on the remote host. 27 | 28 | Everything after `--` will be passed onto the `nixos-rebuild` command. 29 | 30 | ``` 31 | pushnix deploy my_server -- -I nixos-config=/home/user/config/machines/my_server/configuration.nix 32 | ``` 33 | 34 | ## Why? 35 | 36 | [NixOps](https://github.com/NixOS/nixops) and [morph](https://github.com/DBCDK/morph) are much more featureful options that didn't fit one my use cases. 37 | 38 | They build the derivations locally, copy them over to the remote host and run the activation script there. This doesn't make much sense when being run on a laptop where everything is built locally instead on the server itself. 39 | 40 | Also, I want one of my servers to occationally receive updated configuration and then just use `system.autoUpgrade` option to keep it updated. This requires the configuration to live on the remote host. 41 | 42 | This also simplifies testing a configuration on a remote host, instead of pushing to a central repository, SSH into the host, pull from the central repository and finally run `nixos-rebuild`. 43 | -------------------------------------------------------------------------------- /commands/commands.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "github.com/urfave/cli/v2" 5 | 6 | "github.com/arnarg/pushnix/commands/deploy" 7 | "github.com/arnarg/pushnix/commands/push" 8 | ) 9 | 10 | func GetCommands() []*cli.Command { 11 | return []*cli.Command{ 12 | &push.Command, 13 | &deploy.Command, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /commands/deploy/deploy.go: -------------------------------------------------------------------------------- 1 | package deploy 2 | 3 | import ( 4 | "github.com/urfave/cli/v2" 5 | 6 | "github.com/arnarg/pushnix/commands/push" 7 | "github.com/arnarg/pushnix/util" 8 | ) 9 | 10 | var logger = util.Logger 11 | 12 | var Command cli.Command = cli.Command{ 13 | Name: "deploy", 14 | Aliases: []string{"d"}, 15 | Usage: "Deploy local configuration to remote host using git and ssh", 16 | Action: Run, 17 | Before: util.RequiredArgPreFunc, 18 | ArgsUsage: "", 19 | Flags: []cli.Flag{ 20 | &cli.BoolFlag{ 21 | Name: "force", 22 | Aliases: []string{"f"}, 23 | Usage: "Force git push", 24 | }, 25 | &cli.BoolFlag{ 26 | Name: "upgrade", 27 | Aliases: []string{"u"}, 28 | Usage: "Do a channel upgrade with nixos-rebuild", 29 | }, 30 | }, 31 | } 32 | 33 | func Run(c *cli.Context) error { 34 | upgrade := c.Bool("upgrade") 35 | nixExtraArgs := util.ParseTerminator(c.Args().Slice()) 36 | 37 | host, err := push.RunPush(c) 38 | if err != nil { 39 | return err 40 | } 41 | 42 | logger.Printf("Rebuilding NixOS on host %s@%s...\n", host.User, host.Host) 43 | return util.SSHNixosRebuild(host, upgrade, nixExtraArgs) 44 | } 45 | -------------------------------------------------------------------------------- /commands/push/push.go: -------------------------------------------------------------------------------- 1 | package push 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/urfave/cli/v2" 7 | 8 | "github.com/arnarg/pushnix/util" 9 | ) 10 | 11 | var logger = util.Logger 12 | 13 | var Command cli.Command = cli.Command{ 14 | Name: "push", 15 | Aliases: []string{"p"}, 16 | Usage: "Push local configuration to remote host using git", 17 | Action: Run, 18 | Before: util.RequiredArgPreFunc, 19 | ArgsUsage: "", 20 | Flags: []cli.Flag{ 21 | &cli.BoolFlag{ 22 | Name: "force", 23 | Aliases: []string{"f"}, 24 | Usage: "Force git push", 25 | }, 26 | }, 27 | } 28 | 29 | func Run(c *cli.Context) error { 30 | _, err := RunPush(c) 31 | return err 32 | } 33 | 34 | func RunPush(c *cli.Context) (*util.SSHHost, error) { 35 | r := c.Args().Get(0) 36 | force := c.Bool("force") 37 | 38 | host, err := util.GetHostFromRemoteName(r) 39 | if err != nil { 40 | return nil, err 41 | } 42 | if host == nil { 43 | return nil, fmt.Errorf("Could not find remote %s", r) 44 | } 45 | 46 | logger.Printf("Pushing configuration to remote %s...\n", r) 47 | return host, util.GitPush(r, force) 48 | } 49 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import {} }: 2 | 3 | pkgs.mkShell { 4 | buildInputs = with pkgs; [ 5 | gnumake 6 | git 7 | go 8 | ]; 9 | } 10 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/arnarg/pushnix 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/go-git/go-git/v5 v5.1.0 7 | github.com/urfave/cli/v2 v2.2.0 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs= 3 | github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= 4 | github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= 5 | github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= 6 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= 7 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 8 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= 9 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 10 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 11 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 13 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= 15 | github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= 16 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= 17 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= 18 | github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= 19 | github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= 20 | github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= 21 | github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= 22 | github.com/go-git/go-billy/v5 v5.0.0 h1:7NQHvd9FVid8VL4qVUMm8XifBK+2xCoZ2lSk0agRrHM= 23 | github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= 24 | github.com/go-git/go-git-fixtures/v4 v4.0.1 h1:q+IFMfLx200Q3scvt2hN79JsEzy4AmBTp/pqnefH+Bc= 25 | github.com/go-git/go-git-fixtures/v4 v4.0.1/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw= 26 | github.com/go-git/go-git/v5 v5.1.0 h1:HxJn9g/E7eYvKW3Fm7Jt4ee8LXfPOm/H1cdDu8vEssk= 27 | github.com/go-git/go-git/v5 v5.1.0/go.mod h1:ZKfuPUoY1ZqIG4QG9BDBh3G4gLM5zvPuSJAozQrZuyM= 28 | github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= 29 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 30 | github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg= 31 | github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 32 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= 33 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 34 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 35 | github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY= 36 | github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 37 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 38 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 39 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 40 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 41 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 42 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 43 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 44 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 45 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 46 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 47 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 48 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 49 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 50 | github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= 51 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 52 | github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= 53 | github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 54 | github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= 55 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 56 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 57 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 58 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 59 | github.com/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4= 60 | github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= 61 | github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70= 62 | github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= 63 | golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 64 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 65 | golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM= 66 | golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 67 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 68 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0= 69 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 70 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 71 | golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 72 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 73 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So= 74 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 75 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 76 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 77 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 78 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 79 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 80 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 81 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 82 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 83 | gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= 84 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 85 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 86 | gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= 87 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 88 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/urfave/cli/v2" 8 | 9 | "github.com/arnarg/pushnix/commands" 10 | ) 11 | 12 | var Version string = "unknown" 13 | 14 | func main() { 15 | app := &cli.App{ 16 | Name: "pushnix", 17 | Usage: "push configuration to a NixOS host", 18 | Version: Version, 19 | Commands: commands.GetCommands(), 20 | } 21 | 22 | // By default log prints timestamp 23 | // Here I'm removing that 24 | log.SetFlags(0) 25 | 26 | err := app.Run(os.Args) 27 | if err != nil { 28 | log.Fatal(err) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /util/cli_helpers.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/urfave/cli/v2" 7 | ) 8 | 9 | func RequiredArgPreFunc(c *cli.Context) error { 10 | arg := c.Args().Get(0) 11 | if arg == "" || strings.HasPrefix(arg, "-") { 12 | cli.ShowCommandHelpAndExit(c, c.Command.Name, 1) 13 | } 14 | return nil 15 | } 16 | 17 | func ParseTerminator(args []string) []string { 18 | for i, a := range args { 19 | if a == "--" { 20 | return args[i+1:] 21 | } 22 | } 23 | 24 | return []string{} 25 | } 26 | -------------------------------------------------------------------------------- /util/commands.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "strings" 8 | ) 9 | 10 | func GitPush(r string, f bool) error { 11 | args := []string{"push", r} 12 | if f { 13 | args = append(args, "-f") 14 | } 15 | return runCommand("git", args...) 16 | } 17 | 18 | func SSHNixosRebuild(h *SSHHost, u bool, e []string) error { 19 | remoteCommand := "sudo nixos-rebuild switch" 20 | if u { 21 | remoteCommand += " --upgrade" 22 | } 23 | if len(e) > 0 { 24 | remoteCommand = fmt.Sprintf("%s %s", remoteCommand, strings.Join(e, " ")) 25 | } 26 | return runCommand("ssh", "-t", "-l", h.User, h.Host, remoteCommand) 27 | } 28 | 29 | func runCommand(name string, arg ...string) error { 30 | cmd := exec.Command(name, arg...) 31 | cmd.Env = os.Environ() 32 | cmd.Stdout = os.Stdout 33 | cmd.Stderr = os.Stderr 34 | cmd.Stdin = os.Stdin 35 | 36 | err := cmd.Run() 37 | if err != nil { 38 | return err 39 | } 40 | return nil 41 | } 42 | -------------------------------------------------------------------------------- /util/git.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "regexp" 8 | 9 | "github.com/go-git/go-git/v5" 10 | ) 11 | 12 | type SSHHost struct { 13 | User string 14 | Host string 15 | Port string 16 | } 17 | 18 | func GetHostFromRemoteName(n string) (*SSHHost, error) { 19 | wd, err := os.Getwd() 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | gitDir, err := findGitBase(wd) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | repo, err := git.PlainOpen(gitDir) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | remote, err := repo.Remote(n) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | return getHostFromRemote(remote) 40 | } 41 | 42 | func getHostFromRemote(r *git.Remote) (*SSHHost, error) { 43 | for _, u := range r.Config().URLs { 44 | host := parseGitURL(u) 45 | if host != nil { 46 | return host, nil 47 | } 48 | } 49 | return nil, fmt.Errorf("Could not find a URL that matches an SSH string in remote %s", r.Config().Name) 50 | } 51 | 52 | func parseGitURL(u string) *SSHHost { 53 | r := regexp.MustCompile(`^([^@]+)@([^:]+):?(\d*).*$`) 54 | m := r.FindStringSubmatch(u) 55 | if m == nil { 56 | return nil 57 | } 58 | user := m[1] 59 | host := m[2] 60 | port := m[3] 61 | if user == "" || host == "" { 62 | return nil 63 | } 64 | 65 | if port == "" { 66 | port = "22" 67 | } 68 | 69 | return &SSHHost{ 70 | User: user, 71 | Host: host, 72 | Port: port, 73 | } 74 | } 75 | 76 | func findGitBase(p string) (string, error) { 77 | if p == "/" { 78 | return "", fmt.Errorf("not a git repository") 79 | } 80 | if fm, err := os.Stat(p + "/.git"); err == nil && fm.IsDir() { 81 | return p, nil 82 | } 83 | ret, err := findGitBase(filepath.Dir(p)) 84 | return ret, err 85 | } 86 | -------------------------------------------------------------------------------- /util/logger.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "log" 5 | "os" 6 | ) 7 | 8 | var Logger *log.Logger = log.New(os.Stdout, "=> ", 0) 9 | --------------------------------------------------------------------------------