gitkit

package module
v0.0.0-...-ce85db6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 31, 2023 License: MIT Imports: 24 Imported by: 0

README

gitkit

Toolkit to build Git push workflows with Go

Build GoDoc

Install

go get github.com/actor168/gitkit

Smart HTTP Server

package main

import (
  "log"
  "net/http"
  "github.com/actor168/gitkit"
)

func main() {
  // Configure git hooks
  hooks := &gitkit.HookScripts{
    PreReceive: `echo "Hello World!"`,
  }

  // Configure git service
  service := gitkit.New(gitkit.Config{
    Dir:        "/path/to/repos",
    AutoCreate: true,
    AutoHooks:  true,
    Hooks:      hooks,
  })

  // Configure git server. Will create git repos path if it does not exist.
  // If hooks are set, it will also update all repos with new version of hook scripts.
  if err := service.Setup(); err != nil {
    log.Fatal(err)
  }

  http.Handle("/", service)

  // Start HTTP server
  if err := http.ListenAndServe(":5000", nil); err != nil {
    log.Fatal(err)
  }
}

Run example:

go run example.go

Then try to clone a test repository:

$ git clone http://localhost:5000/test.git /tmp/test
# Cloning into '/tmp/test'...
# warning: You appear to have cloned an empty repository.
# Checking connectivity... done.

$ cd /tmp/test
$ touch sample

$ git add sample
$ git commit -am "First commit"
# [master (root-commit) fe40c98] First commit
# 1 file changed, 0 insertions(+), 0 deletions(-)
# create mode 100644 sample

$ git push origin master
# Counting objects: 3, done.
# Writing objects: 100% (3/3), 213 bytes | 0 bytes/s, done.
# Total 3 (delta 0), reused 0 (delta 0)
# remote: Hello World! <----------------- pre-receive hook
# To http://localhost:5000/test.git
# * [new branch]      master -> master

In the example's console you'll see something like this:

2016/05/20 20:01:42 request: GET localhost:5000/test.git/info/refs?service=git-upload-pack
2016/05/20 20:01:42 repo-init: creating pre-receive hook for test.git
2016/05/20 20:03:34 request: GET localhost:5000/test.git/info/refs?service=git-receive-pack
2016/05/20 20:03:34 request: POST localhost:5000/test.git/git-receive-pack
Authentication
package main

import (
  "log"
  "net/http"

  "github.com/actor168/gitkit"
)

func main() {
  service := gitkit.New(gitkit.Config{
    Dir:        "/path/to/repos",
    AutoCreate: true,
    Auth:       true, // Turned off by default
  })

  // Here's the user-defined authentication function.
  // If return value is false or error is set, user's request will be rejected.
  // You can hook up your database/redis/cache for authentication purposes.
  service.AuthFunc = func(cred gitkit.Credential, req *gitkit.Request) (bool, error) {
    log.Println("user auth request for repo:", cred.Username, cred.Password, req.RepoName)
    return cred.Username == "hello", nil
  }

  http.Handle("/", service)
  http.ListenAndServe(":5000", nil)
}

When you start the server and try to clone repo, you'll see password prompt. Two examples below illustrate both failed and succesful authentication based on the auth code above.

$ git clone http://localhost:5000/awesome-sauce.git
# Cloning into 'awesome-sauce'...
# Username for 'http://localhost:5000': foo
# Password for 'http://foo@localhost:5000':
# fatal: Authentication failed for 'http://localhost:5000/awesome-sauce.git/'

$ git clone http://localhost:5000/awesome-sauce.git
# Cloning into 'awesome-sauce'...
# Username for 'http://localhost:5000': hello
# Password for 'http://hello@localhost:5000':
# warning: You appear to have cloned an empty repository.
# Checking connectivity... done.

Git also allows using .netrc files for authentication purposes. Open your ~/.netrc file and add the following line:

machine localhost
  login hello
  password world

Next time you try clone the same localhost git repo, git wont show password promt. Keep in mind that the best practice is to use auth tokens instead of plaintext passwords for authentication. See Heroku's docs for more information.

SSH server

package main

import (
  "log"
  "github.com/actor168/gitkit"
)

// User-defined key lookup function. You can make a call to a database or
// some sort of cache storage (redis/memcached) to speed things up.
// Content is a string containing ssh public key of a user.
func lookupKey(content string) (*gitkit.PublicKey, error) {
  return &gitkit.PublicKey{Id: "12345"}, nil
}

func main() {
  // In the example below you need to specify a full path to a directory that
  // contains all git repositories, and also a directory that has a gitkit specific
  // ssh private and public key pair that used to run ssh server.
  server := gitkit.NewSSH(gitkit.Config{
    Dir:    "/path/to/git/repos",
    KeyDir: "/path/to/gitkit",
  })

  // User-defined key lookup function. All requests will be rejected if this function
  // is not provider. SSH server only accepts key-based authentication.
  server.PublicKeyLookupFunc = lookupKey

  // Specify host and port to run the server on.
  err := server.ListenAndServe(":2222")
  if err != nil {
    log.Fatal(err)
  }
}

Example above uses non-standard SSH port 2222, which can't be used for local testing by default. To make it work you must modify you ssh client configuration file with the following snippet:

$ nano ~/.ssh/config

Paste the following:

Host localhost
  Port 2222

Now that the server is configured, we can fire it up:

$ go run ssh_server.go

First thing you'll need to make sure you have tested the ssh host verification:

$ ssh git@localhost -p 2222
# The authenticity of host '[localhost]:2222 ([::1]:2222)' can't be established.
# RSA key fingerprint is SHA256:eZwC9VSbVnoHFRY9QKGK3aBSUqkShRF0HxFmQyLmBJs.
# Are you sure you want to continue connecting (yes/no)? yes
# Warning: Permanently added '[localhost]:2222' (RSA) to the list of known hosts.
# Unsupported request type.
# Connection to localhost closed.

All good now. Unsupported request type. is a succes output since gitkit does not allow running shell sessions. Assuming you have configured the directory for git repositories, clone the test repo:

$ git clone git@localhost:test.git
# Cloning into 'test'...
# remote: Counting objects: 3, done.
# remote: Total 3 (delta 0), reused 0 (delta 0)
# Receiving objects: 100% (3/3), done.
# Checking connectivity... done.

Done, you have now ability to run git push/pull. The important stuff in all examples above is lookupKey function. It controls whether user is allowd to authenticate with ssh or not.

Receiver

In Git, The first script to run when handling a push from a client is pre-receive. It takes a list of references that are being pushed from stdin; if it exits non-zero, none of them are accepted. More on hooks.

package main

import (
  "log"
  "os"
  "fmt"

  "github.com/actor168/gitkit"
)

// HookInfo contains information about branch, before and after revisions.
// tmpPath is a temporary directory with checked out git tree for the commit.
func receive(hook *gitkit.HookInfo, tmpPath string) error {
  log.Println("Action:", hook.Action)
  log.Println("Ref:", hook.Ref)
  log.Println("Ref name:", hook.RefName)
  log.Println("Old revision:", hook.OldRev)
  log.Println("New revision:", hook.NewRev)

  // Check if push is non fast-forward (force)
  force, err := gitkit.IsForcePush(hook)
  if err != nil {
    return err
  }

  // Reject force push
  if force {
    return fmt.Errorf("non fast-forward pushed are not allowed")
  }

  // Check if branch is being deleted
  if hook.Action == gitkit.BranchDeleteAction {
    fmt.Println("Deleting branch!")
    return nil
  }

  // Getting a commit message is built-in
  message, err := gitkit.ReadCommitMessage(hook.NewRev)
  if err != nil {
    return err
  }
  log.Println("Commit message:", message)

  return nil
}

func main() {
  receiver := gitkit.Receiver{
    MasterOnly:  false,         // if set to true, only pushes to master branch will be allowed
    TmpDir:      "/tmp/gitkit", // directory for temporary git checkouts
    HandlerFunc: receive,       // your handler function
  }

  // Git hook data is provided via STDIN
  if err := receiver.Handle(os.Stdin); err != nil {
    log.Println("Error:", err)
    os.Exit(1) // terminating with non-zero status will cancel push
  }
}

To test if receiver works, you will need to add a sample pre-receive hook to any git repo. With go run its easier to debug but final script should be compiled and will run very fast.

#!/bin/bash
cat | go run /path/to/your-receiver.go

Modify something in the repo, commit the change and push:

$ git push
# Counting objects: 3, done.
# Delta compression using up to 8 threads.
# Compressing objects: 100% (3/3), done.
# Writing objects: 100% (3/3), 286 bytes | 0 bytes/s, done.
# Total 3 (delta 2), reused 0 (delta 0)
# -------------------------- out receiver output is here ----------------
# remote: 2016/05/24 17:21:37 Ref: refs/heads/master
# remote: 2016/05/24 17:21:37 Old revision: 5ee8d0891d1e5574e427dc16e0908cb9d28551b9
# remote: 2016/05/24 17:21:37 New revision: e13d6b3a27403029fe674e7b911efd468b035a33
# remote: 2016/05/24 17:21:37 Message: Remove stuff
# To git@localhost:dummy-app.git
#    5ee8d08..e13d6b3  master -> master

Extras

Remove remote: prefix

If your pre-receive script logs anything to STDOUT, the output might look like this:

# Writing objects: 100% (3/3), 286 bytes | 0 bytes/s, done.
# Total 3 (delta 2), reused 0 (delta 0)
remote: Sample script output <---- YOUR SCRIPT

There's a simple hack to remove this nasty remote: prefix:

#!/bin/bash
/my/receiver-script | sed -u "s/^/"$'\e[1G\e[K'"/"

If you're running on OSX, use gsed instead: brew install gnu-sed.

Result:

# Writing objects: 100% (3/3), 286 bytes | 0 bytes/s, done.
# Total 3 (delta 2), reused 0 (delta 0)
Sample script output

References

License

The MIT License

Copyright (c) 2016-2023 Dan Sosedoff, dan.sosedoff@gmail.com

Documentation

Index

Constants

View Source
const (
	BranchPushAction   = "branch.push"
	BranchCreateAction = "branch.create"
	BranchDeleteAction = "branch.delete"
	TagCreateAction    = "tag.create"
	TagDeleteAction    = "tag.delete"
)
View Source
const Version = "0.4.0"
View Source
const ZeroSHA = "0000000000000000000000000000000000000000"

Variables

View Source
var (
	ErrAlreadyStarted = errors.New("server has already been started")
	ErrNoListener     = errors.New("cannot call Serve() before Listen()")
)

Functions

func IsForcePush

func IsForcePush(hook *HookInfo) (bool, error)

func ReadCommitMessage

func ReadCommitMessage(sha string) (string, error)

Types

type Config

type Config struct {
	KeyDir     string       // Directory for server ssh keys. Only used in SSH strategy.
	Dir        string       // Directory that contains repositories
	GitPath    string       // Path to git binary
	GitUser    string       // User for ssh connections
	AutoCreate bool         // Automatically create repostories
	AutoHooks  bool         // Automatically setup git hooks
	Hooks      *HookScripts // Scripts for hooks/* directory
	Auth       bool         // Require authentication
}

func (*Config) KeyPath

func (c *Config) KeyPath() string

func (*Config) Setup

func (c *Config) Setup() error

type Credential

type Credential struct {
	Username string
	Password string
	Token    string
}

type GitCommand

type GitCommand struct {
	Command  string
	Repo     string
	Original string
}

func ParseGitCommand

func ParseGitCommand(cmd string) (*GitCommand, error)

type HookInfo

type HookInfo struct {
	Action   string
	RepoName string
	RepoPath string
	OldRev   string
	NewRev   string
	Ref      string
	RefType  string
	RefName  string
}

HookInfo holds git hook context

func ReadHookInput

func ReadHookInput(input io.Reader) (*HookInfo, error)

ReadHookInput reads the hook context

type HookScripts

type HookScripts struct {
	PreReceive  string
	Update      string
	PostReceive string
}

HookScripts represents all repository server-size git hooks

type KitListRepoResponse

type KitListRepoResponse struct {
	RepoPath []string `json:"repoPath"`
}

type KitRepoResponse

type KitRepoResponse struct {
	RepoPath string `json:"repoPath"`
}

type KitResponse

type KitResponse struct {
	Code int         `json:"code"`
	Data interface{} `json:"data"`
}

type PublicKey

type PublicKey struct {
	Id          string
	Name        string
	Fingerprint string
	Content     string
}

type Receiver

type Receiver struct {
	Debug       bool
	MasterOnly  bool
	TmpDir      string
	HandlerFunc func(*HookInfo, string) error
}

func (*Receiver) Handle

func (r *Receiver) Handle(reader io.Reader) error

type Request

type Request struct {
	*http.Request
	RepoName string
	RepoPath string
}

type SSH

type SSH struct {
	PublicKeyLookupFunc func(string) (*PublicKey, error)
	// contains filtered or unexported fields
}

func NewSSH

func NewSSH(config Config) *SSH

func (*SSH) Address

func (s *SSH) Address() string

Address returns the network address of the listener. This is in particular useful when binding to :0 to get a free port assigned by the OS.

func (*SSH) Listen

func (s *SSH) Listen(bind string) error

func (*SSH) ListenAndServe

func (s *SSH) ListenAndServe(bind string) error

func (*SSH) Serve

func (s *SSH) Serve() error

func (*SSH) SetListener

func (s *SSH) SetListener(l net.Listener)

SetListener can be used to set custom Listener.

func (*SSH) SetSSHConfig

func (s *SSH) SetSSHConfig(cfg *ssh.ServerConfig)

SetSSHConfig can be used to set custom SSH Server settings.

func (*SSH) Stop

func (s *SSH) Stop() error

Stop stops the server if it has been started, otherwise it is a no-op.

type Server

type Server struct {
	AuthFunc       func(Credential, *Request) (bool, error)
	FilterRepoFunc func([]string, *Request) []string
	// contains filtered or unexported fields
}

func New

func New(cfg Config) *Server

func (*Server) ServeHTTP

func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request)

func (*Server) Setup

func (s *Server) Setup() error

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL