From 54af1ee510e54bb95ce272ef748660db8c11a477 Mon Sep 17 00:00:00 2001 From: mediocregopher <> Date: Sat, 4 Apr 2020 14:13:57 -0600 Subject: [PATCH] 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 --- ROADMAP.md | 1 - cmd/dehub-remote/pre-receive | 3 - cmd/dehub-remote/start.sh | 10 +-- cmd/dehub/cmd_misc.go | 27 +++++++ cmd/dehub/main.go | 7 +- repo.go | 153 ++++++++++++++++++++++++++++++++--- 6 files changed, 173 insertions(+), 28 deletions(-) delete mode 100644 cmd/dehub-remote/pre-receive create mode 100644 cmd/dehub/cmd_misc.go diff --git a/ROADMAP.md b/ROADMAP.md index 01b1637..2859ea4 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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 diff --git a/cmd/dehub-remote/pre-receive b/cmd/dehub-remote/pre-receive deleted file mode 100644 index 60e12ef..0000000 --- a/cmd/dehub-remote/pre-receive +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -exec dehub -bare hook --pre-receive diff --git a/cmd/dehub-remote/start.sh b/cmd/dehub-remote/start.sh index 086d0da..77e51f5 100644 --- a/cmd/dehub-remote/start.sh +++ b/cmd/dehub-remote/start.sh @@ -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 diff --git a/cmd/dehub/cmd_misc.go b/cmd/dehub/cmd_misc.go new file mode 100644 index 0000000..29e9a6f --- /dev/null +++ b/cmd/dehub/cmd_misc.go @@ -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 + }) +} diff --git a/cmd/dehub/main.go b/cmd/dehub/main.go index a837ec2..569bf73 100644 --- a/cmd/dehub/main.go +++ b/cmd/dehub/main.go @@ -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) { diff --git a/repo.go b/repo.go index 035be81..1d3d4bf 100644 --- a/repo.go +++ b/repo.go @@ -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 }