add init command

---
type: change
message: |-
  add init command

  This ended up being bigger than expected. I decided to make the init command
  also handle setting up the git config and pre-receive hook for remote-enabled
  repositories, which took me on a bit of a journey through how go-git handles the
  filesystem. In the end this greatly simplifies dehub-remote, and will make
  getting new people set up with a repo much more straightforward.
change_hash: AHlWg77eGGr071jVIMJtv+DU3U9x+tcTUsegt4yrk/9W
credentials:
- type: pgp_signature
  pub_key_id: 95C46FA6A41148AC
  body: iQIzBAABAgAdFiEEJ6tQKp6olvZKJ0lwlcRvpqQRSKwFAl6I6oAACgkQlcRvpqQRSKw+dxAAjirfVfIsudnYo3TJxaR1qhQmJJg7v2aMnRQXgYKHEpy8Z1m994WyH3sGJhDQsx3Z/26XjW7fOgXUUKnqZNLp+js182yNo855Ik0vUuEHunAy+YXkBklo3AlkQSJlug9kJrYYjKH4PsJnPZ5JcHXyauxcaydFwHQP61+wiuX/y4iVP/T79ljqbe8GXVj50+rzhu1cRGEgUswnBNSVLYyecU+aEOejfb5CMoCsVtFKunHc+PyHmFJ1XVTwhF/wsCI3pxKYJ/wGqTW6urV46vZhp4SZIJFbSaQkIWuEUXg4JDp8prqgt2IdVlK/wfjxe7YXB9YpzcC4Wblc+m6tiX2lIuEogTyaRK31A2M5/f4m7f7DnPddbZ/fKQ2BBLyLJTtGSQxaFc6LzEmjgMUoUGvkGEPHcaZ1CAOOdcEYn3Y1uwwh4+BJhKPdfpHY4c10+F1Dhi86A4Pi/QmUZ0Cgr9u08wMg1efvD1VTZ6QPX9tpTKi0YqflFdDCDKlLtuA9AvTKBOGGC/XOCmCGjqFpptVRAf+Fl/7DtPG1TUVnKAs1CIw85cMYtEvTptCnMNkwnRFpwiFy856RvpR7RUiQfkWkdohwUY5psFfggOktTgydm5nULWZ4AzZW430MIeJj9XH8FKvYfTN08T9C1kYCwMtC9HYMmXuPdCJNuymQNBAWgaY=
  account: mediocregopher
main
mediocregopher 4 years ago
parent d189d46667
commit 54af1ee510
  1. 1
      ROADMAP.md
  2. 3
      cmd/dehub-remote/pre-receive
  3. 10
      cmd/dehub-remote/start.sh
  4. 27
      cmd/dehub/cmd_misc.go
  5. 7
      cmd/dehub/main.go
  6. 153
      repo.go

@ -11,7 +11,6 @@ to accept help from people asking to help.
* Restrict new branches so that they must be ancestors of main.
* Fast-forward perms on branches (so they can be deleted)
* `init` command.
* Ammending commits.
* Figure out commit range syntax, use that everywhere.
* Support short hash names

@ -1,3 +0,0 @@
#!/bin/sh
exec dehub -bare hook --pre-receive

@ -71,17 +71,9 @@ while [ ! -z "$1" ]; do
if [ ! -d "$dir" ]; then
echo "Initializing repo $1"
mkdir "$dir"
git init --bare "$dir"
git config -f "$dir/config" http.receivepack true
git config -f "$dir/config" receive.denyNonFastForwards true
git symbolic-ref HEAD refs/heads/main
dehub init -path "$dir" -bare -remote
chown -R git:git "$dir"
fi
mkdir -p "$dir/hooks"
cp /pre-receive "$dir/hooks/"
chmod +x "$dir/hooks/pre-receive"
shift
done

@ -0,0 +1,27 @@
package main
import (
"context"
"fmt"
"dehub.dev/src/dehub.git"
"dehub.dev/src/dehub.git/cmd/dehub/dcmd"
)
func cmdInit(ctx context.Context, cmd *dcmd.Cmd) {
flag := cmd.FlagSet()
path := flag.String("path", ".", "Path to initialize the repo at")
bare := flag.Bool("bare", false, "Initialize the repo as a bare repository")
remote := flag.Bool("remote", false, "Configure the directory to allow it to be used as a remote endpoint")
cmd.Run(func() (context.Context, error) {
_, err := dehub.InitRepo(*path,
dehub.InitBare(*bare),
dehub.InitRemote(*remote),
)
if err != nil {
return nil, fmt.Errorf("initializing repo at %q: %w", *path, err)
}
return nil, nil
})
}

@ -8,9 +8,10 @@ import (
func main() {
cmd := dcmd.New()
cmd.SubCmd("commit", "commits staged changes to the head of the current branch", cmdCommit)
cmd.SubCmd("verify", "verifies one or more commits as having the proper credentials", cmdVerify)
cmd.SubCmd("hook", "use dehub as a git hook", cmdHook)
cmd.SubCmd("init", "Initialize a new repository in a directory", cmdInit)
cmd.SubCmd("commit", "Commits staged changes to the head of the current branch", cmdCommit)
cmd.SubCmd("verify", "Verifies one or more commits as having the proper credentials", cmdVerify)
cmd.SubCmd("hook", "Use dehub as a git hook", cmdHook)
cmd.SubCmd("combine", "Combine multiple change and credential commits into a single commit", cmdCombine)
cmd.Run(func() (context.Context, error) {

@ -2,18 +2,24 @@
package dehub
import (
"dehub.dev/src/dehub.git/fs"
"bytes"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"dehub.dev/src/dehub.git/fs"
"gopkg.in/src-d/go-billy.v4"
"gopkg.in/src-d/go-billy.v4/memfs"
"gopkg.in/src-d/go-git.v4"
"gopkg.in/src-d/go-git.v4/plumbing"
"gopkg.in/src-d/go-git.v4/plumbing/cache"
"gopkg.in/src-d/go-git.v4/plumbing/format/config"
"gopkg.in/src-d/go-git.v4/plumbing/object"
"gopkg.in/src-d/go-git.v4/storage"
"gopkg.in/src-d/go-git.v4/storage/memory"
"gopkg.in/src-d/go-git.v4/storage/filesystem"
)
const (
@ -33,18 +39,18 @@ var (
MainRefName = plumbing.NewBranchReferenceName(Main)
)
type repoOpts struct {
type openOpts struct {
bare bool
}
// OpenOption is an option which can be passed to the OpenRepo function to
// affect the Repo's behavior.
type OpenOption func(*repoOpts)
type OpenOption func(*openOpts)
// OpenBare returns an OpenOption which, if true is given, causes the OpenRepo
// function to expect to open a bare repo.
func OpenBare(bare bool) OpenOption {
return func(o *repoOpts) {
return func(o *openOpts) {
o.bare = bare
}
}
@ -52,7 +58,19 @@ func OpenBare(bare bool) OpenOption {
// Repo is an object which allows accessing and modifying the dehub repo.
type Repo struct {
GitRepo *git.Repository
Storer storage.Storer
// GitDirFS corresponds to the .git directory (or the entire repo directory
// if it's a bare repo)
GitDirFS billy.Filesystem
}
func extractGitDirFS(storer storage.Storer) (billy.Filesystem, error) {
dotGitFSer, ok := storer.(interface{ Filesystem() billy.Filesystem })
if !ok {
return nil, fmt.Errorf("git storage object of type %T does not expose its underlying filesystem",
storer)
}
return dotGitFSer.Filesystem(), nil
}
// OpenRepo opens the dehub repo in the given directory and returns the object
@ -61,7 +79,7 @@ type Repo struct {
// The given path is expected to have a git repo and .dehub folder already
// initialized.
func OpenRepo(path string, options ...OpenOption) (*Repo, error) {
var opts repoOpts
var opts openOpts
for _, opt := range options {
opt(&opts)
}
@ -73,30 +91,141 @@ func OpenRepo(path string, options ...OpenOption) (*Repo, error) {
}
if r.GitRepo, err = git.PlainOpenWithOptions(path, openOpts); err != nil {
return nil, fmt.Errorf("could not open git repo: %w", err)
} else if r.GitDirFS, err = extractGitDirFS(r.GitRepo.Storer); err != nil {
return nil, err
}
return &r, nil
}
type initOpts struct {
bare bool
remote bool
}
// InitOption is an option which can be passed into the Init functions to affect
// their behavior.
type InitOption func(*initOpts)
// InitBare returns an InitOption which, if true is given, causes the Init
// function to initialize the repo without a worktree.
func InitBare(bare bool) InitOption {
return func(o *initOpts) {
o.bare = bare
}
}
// InitRemote returns an InitOption which, if true is given, causes the Init
// function to initialize the repo with certain git configuration options set
// which make the repo able to be used as a remote repo.
func InitRemote(remote bool) InitOption {
return func(o *initOpts) {
o.remote = remote
}
}
// InitRepo will initialize a new repository at the given path. If bare is true
// then the repository will not have a worktree.
func InitRepo(path string, options ...InitOption) (*Repo, error) {
var opts initOpts
for _, opt := range options {
opt(&opts)
}
var repo Repo
var err error
if repo.GitRepo, err = git.PlainInit(path, opts.bare); err != nil {
return nil, fmt.Errorf("initializing git repo: %w", err)
} else if repo.GitDirFS, err = extractGitDirFS(repo.GitRepo.Storer); err != nil {
return nil, err
} else if err = repo.init(opts); err != nil {
return nil, fmt.Errorf("initializing repo with dehub defaults: %w", err)
}
return &repo, nil
}
// InitMemRepo initializes an empty repository which only exists in memory.
func InitMemRepo() *Repo {
r, err := git.Init(memory.NewStorage(), memfs.New())
func InitMemRepo(options ...InitOption) *Repo {
var opts initOpts
for _, opt := range options {
opt(&opts)
}
fs := memfs.New()
dotGitFS, err := fs.Chroot(git.GitDirName)
if err != nil {
panic(err)
}
storage := filesystem.NewStorage(dotGitFS, cache.NewObjectLRUDefault())
repo := &Repo{GitRepo: r}
if err := repo.init(); err != nil {
var worktree billy.Filesystem
if !opts.bare {
worktree = fs
}
r, err := git.Init(storage, worktree)
if err != nil {
panic(err)
}
repo := &Repo{GitRepo: r, GitDirFS: dotGitFS}
if err := repo.init(opts); err != nil {
panic(err)
}
return repo
}
func (r *Repo) init() error {
func (r *Repo) initRemotePreReceive(bare bool) error {
if err := r.GitDirFS.MkdirAll("hooks", 0755); err != nil {
return fmt.Errorf("creating hooks directory: %w", err)
}
preRcvFlags := os.O_WRONLY | os.O_CREATE | os.O_TRUNC
preRcv, err := r.GitDirFS.OpenFile("hooks/pre-receive", preRcvFlags, 0755)
if err != nil {
return fmt.Errorf("opening hooks/pre-receive file: %w", err)
}
defer preRcv.Close()
var preRcvBody string
if bare {
preRcvBody = "#!/bin/sh\nexec dehub hook -bare -pre-receive\n"
} else {
preRcvBody = "#!/bin/sh\nexec dehub hook -pre-receive\n"
}
if _, err := io.Copy(preRcv, bytes.NewBufferString(preRcvBody)); err != nil {
return fmt.Errorf("writing to hooks/pre-receive: %w", err)
}
return nil
}
func (r *Repo) init(opts initOpts) error {
headRef := plumbing.NewSymbolicReference(plumbing.HEAD, MainRefName)
if err := r.GitRepo.Storer.SetReference(headRef); err != nil {
return fmt.Errorf("setting HEAD reference to %q: %w", MainRefName, err)
}
if opts.remote {
cfg, err := r.GitRepo.Config()
if err != nil {
return fmt.Errorf("opening git cfg: %w", err)
}
cfg.Raw = cfg.Raw.AddOption("http", config.NoSubsection, "receivepack", "true")
if err := r.GitRepo.Storer.SetConfig(cfg); err != nil {
return fmt.Errorf("storing modified git config: %w", err)
}
if err := r.initRemotePreReceive(opts.bare); err != nil {
return fmt.Errorf("initializing pre-receive hook for remote-enabled repo: %w", err)
}
}
return nil
}

Loading…
Cancel
Save