diff options
-rw-r--r-- | cache/bug_cache.go | 137 | ||||
-rw-r--r-- | cache/cache.go | 342 | ||||
-rw-r--r-- | cache/repo_cache.go | 144 | ||||
-rw-r--r-- | commands/root.go | 9 | ||||
-rw-r--r-- | commands/webui.go | 58 | ||||
-rw-r--r-- | graphql/handler.go | 20 | ||||
-rw-r--r-- | util/process.go | 23 |
7 files changed, 478 insertions, 255 deletions
diff --git a/cache/bug_cache.go b/cache/bug_cache.go new file mode 100644 index 00000000..59c39f5c --- /dev/null +++ b/cache/bug_cache.go @@ -0,0 +1,137 @@ +package cache + +import ( + "github.com/MichaelMure/git-bug/bug" + "github.com/MichaelMure/git-bug/bug/operations" + "github.com/MichaelMure/git-bug/repository" + "github.com/MichaelMure/git-bug/util" +) + +type BugCacher interface { + Snapshot() *bug.Snapshot + ClearSnapshot() + + // Mutations + AddComment(message string) error + AddCommentWithFiles(message string, files []util.Hash) error + ChangeLabels(added []string, removed []string) error + Open() error + Close() error + SetTitle(title string) error + + Commit() error + CommitAsNeeded() error +} + +type BugCache struct { + repo repository.Repo + bug *bug.Bug + snap *bug.Snapshot +} + +func NewBugCache(repo repository.Repo, b *bug.Bug) BugCacher { + return &BugCache{ + repo: repo, + bug: b, + } +} + +func (c *BugCache) Snapshot() *bug.Snapshot { + if c.snap == nil { + snap := c.bug.Compile() + c.snap = &snap + } + return c.snap +} + +func (c *BugCache) ClearSnapshot() { + c.snap = nil +} + +func (c *BugCache) AddComment(message string) error { + return c.AddCommentWithFiles(message, nil) +} + +func (c *BugCache) AddCommentWithFiles(message string, files []util.Hash) error { + author, err := bug.GetUser(c.repo) + if err != nil { + return err + } + + operations.CommentWithFiles(c.bug, author, message, files) + + // TODO: perf --> the snapshot could simply be updated with the new op + c.ClearSnapshot() + + return nil +} + +func (c *BugCache) ChangeLabels(added []string, removed []string) error { + author, err := bug.GetUser(c.repo) + if err != nil { + return err + } + + err = operations.ChangeLabels(nil, c.bug, author, added, removed) + if err != nil { + return err + } + + // TODO: perf --> the snapshot could simply be updated with the new op + c.ClearSnapshot() + + return nil +} + +func (c *BugCache) Open() error { + author, err := bug.GetUser(c.repo) + if err != nil { + return err + } + + operations.Open(c.bug, author) + + // TODO: perf --> the snapshot could simply be updated with the new op + c.ClearSnapshot() + + return nil +} + +func (c *BugCache) Close() error { + author, err := bug.GetUser(c.repo) + if err != nil { + return err + } + + operations.Close(c.bug, author) + + // TODO: perf --> the snapshot could simply be updated with the new op + c.ClearSnapshot() + + return nil +} + +func (c *BugCache) SetTitle(title string) error { + author, err := bug.GetUser(c.repo) + if err != nil { + return err + } + + operations.SetTitle(c.bug, author, title) + + // TODO: perf --> the snapshot could simply be updated with the new op + c.ClearSnapshot() + + return nil +} + +func (c *BugCache) Commit() error { + return c.bug.Commit(c.repo) +} + +func (c *BugCache) CommitAsNeeded() error { + if c.bug.HasPendingOp() { + return c.bug.Commit(c.repo) + } + return nil +} diff --git a/cache/cache.go b/cache/cache.go index c4177f75..618ec981 100644 --- a/cache/cache.go +++ b/cache/cache.go @@ -3,56 +3,32 @@ package cache import ( "fmt" "io" - "strings" + "io/ioutil" + "os" + "path" + "strconv" - "github.com/MichaelMure/git-bug/bug" - "github.com/MichaelMure/git-bug/bug/operations" "github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/util" ) +const lockfile = "lock" + type Cacher interface { - RegisterRepository(ref string, repo repository.Repo) - RegisterDefaultRepository(repo repository.Repo) + // RegisterRepository register a named repository. Use this for multi-repo setup + RegisterRepository(ref string, repo repository.Repo) error + // RegisterDefaultRepository register a unnamed repository. Use this for mono-repo setup + RegisterDefaultRepository(repo repository.Repo) error + // ResolveRepo retrieve a repository by name ResolveRepo(ref string) (RepoCacher, error) + // DefaultRepo retrieve the default repository DefaultRepo() (RepoCacher, error) -} - -type RepoCacher interface { - Repository() repository.Repo - ResolveBug(id string) (BugCacher, error) - ResolveBugPrefix(prefix string) (BugCacher, error) - AllBugIds() ([]string, error) - ClearAllBugs() - - // Mutations - NewBug(title string, message string) (BugCacher, error) - NewBugWithFiles(title string, message string, files []util.Hash) (BugCacher, error) - Fetch(remote string) (string, error) - MergeAll(remote string) <-chan bug.MergeResult - Pull(remote string, out io.Writer) error - Push(remote string) (string, error) -} - -type BugCacher interface { - Snapshot() *bug.Snapshot - ClearSnapshot() - // Mutations - AddComment(message string) error - AddCommentWithFiles(message string, files []util.Hash) error - ChangeLabels(added []string, removed []string) error - Open() error + // Close will do anything that is needed to close the cache properly Close() error - SetTitle(title string) error - - Commit() error - CommitAsNeeded() error } -// Cacher ------------------------ - type RootCache struct { repos map[string]RepoCacher } @@ -63,263 +39,139 @@ func NewCache() RootCache { } } -func (c *RootCache) RegisterRepository(ref string, repo repository.Repo) { - c.repos[ref] = NewRepoCache(repo) -} - -func (c *RootCache) RegisterDefaultRepository(repo repository.Repo) { - c.repos[""] = NewRepoCache(repo) -} - -func (c *RootCache) DefaultRepo() (RepoCacher, error) { - if len(c.repos) != 1 { - return nil, fmt.Errorf("repository is not unique") - } - - for _, r := range c.repos { - return r, nil - } - - panic("unreachable") -} - -func (c *RootCache) ResolveRepo(ref string) (RepoCacher, error) { - r, ok := c.repos[ref] - if !ok { - return nil, fmt.Errorf("unknown repo") - } - return r, nil -} - -// Repo ------------------------ - -type RepoCache struct { - repo repository.Repo - bugs map[string]BugCacher -} - -func NewRepoCache(r repository.Repo) RepoCacher { - return &RepoCache{ - repo: r, - bugs: make(map[string]BugCacher), - } -} - -func (c *RepoCache) Repository() repository.Repo { - return c.repo -} - -func (c *RepoCache) ResolveBug(id string) (BugCacher, error) { - cached, ok := c.bugs[id] - if ok { - return cached, nil - } - - b, err := bug.ReadLocalBug(c.repo, id) +// RegisterRepository register a named repository. Use this for multi-repo setup +func (c *RootCache) RegisterRepository(ref string, repo repository.Repo) error { + err := c.lockRepository(repo) if err != nil { - return nil, err + return err } - cached = NewBugCache(c.repo, b) - c.bugs[id] = cached - - return cached, nil + c.repos[ref] = NewRepoCache(repo) + return nil } -func (c *RepoCache) ResolveBugPrefix(prefix string) (BugCacher, error) { - // preallocate but empty - matching := make([]string, 0, 5) - - for id := range c.bugs { - if strings.HasPrefix(id, prefix) { - matching = append(matching, id) - } - } - - // TODO: should check matching bug in the repo as well - - if len(matching) > 1 { - return nil, fmt.Errorf("Multiple matching bug found:\n%s", strings.Join(matching, "\n")) - } - - if len(matching) == 1 { - b := c.bugs[matching[0]] - return b, nil - } - - b, err := bug.FindLocalBug(c.repo, prefix) - +// RegisterDefaultRepository register a unnamed repository. Use this for mono-repo setup +func (c *RootCache) RegisterDefaultRepository(repo repository.Repo) error { + err := c.lockRepository(repo) if err != nil { - return nil, err + return err } - cached := NewBugCache(c.repo, b) - c.bugs[b.Id()] = cached - - return cached, nil -} - -func (c *RepoCache) AllBugIds() ([]string, error) { - return bug.ListLocalIds(c.repo) -} - -func (c *RepoCache) ClearAllBugs() { - c.bugs = make(map[string]BugCacher) + c.repos[""] = NewRepoCache(repo) + return nil } -func (c *RepoCache) NewBug(title string, message string) (BugCacher, error) { - return c.NewBugWithFiles(title, message, nil) -} +func (c *RootCache) lockRepository(repo repository.Repo) error { + lockPath := repoLockFilePath(repo) -func (c *RepoCache) NewBugWithFiles(title string, message string, files []util.Hash) (BugCacher, error) { - author, err := bug.GetUser(c.repo) + err := RepoIsAvailable(repo) if err != nil { - return nil, err + return err } - b, err := operations.CreateWithFiles(author, title, message, files) + f, err := os.Create(lockPath) if err != nil { - return nil, err + return err } - err = b.Commit(c.repo) + pid := fmt.Sprintf("%d", os.Getpid()) + _, err = f.WriteString(pid) if err != nil { - return nil, err + return err } - cached := NewBugCache(c.repo, b) - c.bugs[b.Id()] = cached - - return cached, nil -} - -func (c *RepoCache) Fetch(remote string) (string, error) { - return bug.Fetch(c.repo, remote) -} - -func (c *RepoCache) MergeAll(remote string) <-chan bug.MergeResult { - return bug.MergeAll(c.repo, remote) -} - -func (c *RepoCache) Pull(remote string, out io.Writer) error { - return bug.Pull(c.repo, out, remote) + return f.Close() } -func (c *RepoCache) Push(remote string) (string, error) { - return bug.Push(c.repo, remote) -} - -// Bug ------------------------ - -type BugCache struct { - repo repository.Repo - bug *bug.Bug - snap *bug.Snapshot -} - -func NewBugCache(repo repository.Repo, b *bug.Bug) BugCacher { - return &BugCache{ - repo: repo, - bug: b, +// ResolveRepo retrieve a repository by name +func (c *RootCache) DefaultRepo() (RepoCacher, error) { + if len(c.repos) != 1 { + return nil, fmt.Errorf("repository is not unique") } -} -func (c *BugCache) Snapshot() *bug.Snapshot { - if c.snap == nil { - snap := c.bug.Compile() - c.snap = &snap + for _, r := range c.repos { + return r, nil } - return c.snap -} -func (c *BugCache) ClearSnapshot() { - c.snap = nil -} - -func (c *BugCache) AddComment(message string) error { - return c.AddCommentWithFiles(message, nil) + panic("unreachable") } -func (c *BugCache) AddCommentWithFiles(message string, files []util.Hash) error { - author, err := bug.GetUser(c.repo) - if err != nil { - return err +// DefaultRepo retrieve the default repository +func (c *RootCache) ResolveRepo(ref string) (RepoCacher, error) { + r, ok := c.repos[ref] + if !ok { + return nil, fmt.Errorf("unknown repo") } - - operations.CommentWithFiles(c.bug, author, message, files) - - // TODO: perf --> the snapshot could simply be updated with the new op - c.ClearSnapshot() - - return nil + return r, nil } -func (c *BugCache) ChangeLabels(added []string, removed []string) error { - author, err := bug.GetUser(c.repo) - if err != nil { - return err - } - - err = operations.ChangeLabels(nil, c.bug, author, added, removed) - if err != nil { - return err +func (c *RootCache) Close() error { + for _, cachedRepo := range c.repos { + lockPath := repoLockFilePath(cachedRepo.Repository()) + err := os.Remove(lockPath) + if err != nil { + return err + } } - - // TODO: perf --> the snapshot could simply be updated with the new op - c.ClearSnapshot() - return nil } -func (c *BugCache) Open() error { - author, err := bug.GetUser(c.repo) - if err != nil { - return err - } +func RepoIsAvailable(repo repository.Repo) error { + lockPath := repoLockFilePath(repo) - operations.Open(c.bug, author) + // Todo: this leave way for a racey access to the repo between the test + // if the file exist and the actual write. It's probably not a problem in + // practice because using a repository will be done from user interaction + // or in a context where a single instance of git-bug is already guaranteed + // (say, a server with the web UI running). But still, that might be nice to + // have a mutex or something to guard that. - // TODO: perf --> the snapshot could simply be updated with the new op - c.ClearSnapshot() + // Todo: this will fail if somehow the filesystem is shared with another + // computer. Should add a configuration that prevent the cleaning of the + // lock file - return nil -} + f, err := os.Open(lockPath) -func (c *BugCache) Close() error { - author, err := bug.GetUser(c.repo) - if err != nil { + if err != nil && !os.IsNotExist(err) { return err } - operations.Close(c.bug, author) + if err == nil { + // lock file already exist + buf, err := ioutil.ReadAll(io.LimitReader(f, 10)) + if err != nil { + return err + } + if len(buf) == 10 { + return fmt.Errorf("The lock file should be < 10 bytes") + } - // TODO: perf --> the snapshot could simply be updated with the new op - c.ClearSnapshot() + pid, err := strconv.Atoi(string(buf)) + if err != nil { + return err + } - return nil -} + if util.ProcessIsRunning(pid) { + return fmt.Errorf("The repository you want to access is already locked by the process pid %d", pid) + } -func (c *BugCache) SetTitle(title string) error { - author, err := bug.GetUser(c.repo) - if err != nil { - return err - } + // The lock file is just laying there after a crash, clean it - operations.SetTitle(c.bug, author, title) + fmt.Println("A lock file is present but the corresponding process is not, removing it.") + err = f.Close() + if err != nil { + return err + } - // TODO: perf --> the snapshot could simply be updated with the new op - c.ClearSnapshot() + os.Remove(lockPath) + if err != nil { + return err + } + } return nil } -func (c *BugCache) Commit() error { - return c.bug.Commit(c.repo) -} - -func (c *BugCache) CommitAsNeeded() error { - if c.bug.HasPendingOp() { - return c.bug.Commit(c.repo) - } - return nil +func repoLockFilePath(repo repository.Repo) string { + return path.Join(repo.GetPath(), ".git", "git-bug", lockfile) } diff --git a/cache/repo_cache.go b/cache/repo_cache.go new file mode 100644 index 00000000..e58165d2 --- /dev/null +++ b/cache/repo_cache.go @@ -0,0 +1,144 @@ +package cache + +import ( + "fmt" + "io" + "strings" + + "github.com/MichaelMure/git-bug/bug" + "github.com/MichaelMure/git-bug/bug/operations" + "github.com/MichaelMure/git-bug/repository" + "github.com/MichaelMure/git-bug/util" +) + +type RepoCacher interface { + Repository() repository.Repo + ResolveBug(id string) (BugCacher, error) + ResolveBugPrefix(prefix string) (BugCacher, error) + AllBugIds() ([]string, error) + ClearAllBugs() + + // Mutations + NewBug(title string, message string) (BugCacher, error) + NewBugWithFiles(title string, message string, files []util.Hash) (BugCacher, error) + Fetch(remote string) (string, error) + MergeAll(remote string) <-chan bug.MergeResult + Pull(remote string, out io.Writer) error + Push(remote string) (string, error) +} + +type RepoCache struct { + repo repository.Repo + bugs map[string]BugCacher +} + +func NewRepoCache(r repository.Repo) RepoCacher { + return &RepoCache{ + repo: r, + bugs: make(map[string]BugCacher), + } +} + +func (c *RepoCache) Repository() repository.Repo { + return c.repo +} + +func (c *RepoCache) ResolveBug(id string) (BugCacher, error) { + cached, ok := c.bugs[id] + if ok { + return cached, nil + } + + b, err := bug.ReadLocalBug(c.repo, id) + if err != nil { + return nil, err + } + + cached = NewBugCache(c.repo, b) + c.bugs[id] = cached + + return cached, nil +} + +func (c *RepoCache) ResolveBugPrefix(prefix string) (BugCacher, error) { + // preallocate but empty + matching := make([]string, 0, 5) + + for id := range c.bugs { + if strings.HasPrefix(id, prefix) { + matching = append(matching, id) + } + } + + // TODO: should check matching bug in the repo as well + + if len(matching) > 1 { + return nil, fmt.Errorf("Multiple matching bug found:\n%s", strings.Join(matching, "\n")) + } + + if len(matching) == 1 { + b := c.bugs[matching[0]] + return b, nil + } + + b, err := bug.FindLocalBug(c.repo, prefix) + + if err != nil { + return nil, err + } + + cached := NewBugCache(c.repo, b) + c.bugs[b.Id()] = cached + + return cached, nil +} + +func (c *RepoCache) AllBugIds() ([]string, error) { + return bug.ListLocalIds(c.repo) +} + +func (c *RepoCache) ClearAllBugs() { + c.bugs = make(map[string]BugCacher) +} + +func (c *RepoCache) NewBug(title string, message string) (BugCacher, error) { + return c.NewBugWithFiles(title, message, nil) +} + +func (c *RepoCache) NewBugWithFiles(title string, message string, files []util.Hash) (BugCacher, error) { + author, err := bug.GetUser(c.repo) + if err != nil { + return nil, err + } + + b, err := operations.CreateWithFiles(author, title, message, files) + if err != nil { + return nil, err + } + + err = b.Commit(c.repo) + if err != nil { + return nil, err + } + + cached := NewBugCache(c.repo, b) + c.bugs[b.Id()] = cached + + return cached, nil +} + +func (c *RepoCache) Fetch(remote string) (string, error) { + return bug.Fetch(c.repo, remote) +} + +func (c *RepoCache) MergeAll(remote string) <-chan bug.MergeResult { + return bug.MergeAll(c.repo, remote) +} + +func (c *RepoCache) Pull(remote string, out io.Writer) error { + return bug.Pull(c.repo, out, remote) +} + +func (c *RepoCache) Push(remote string) (string, error) { + return bug.Push(c.repo, remote) +} diff --git a/commands/root.go b/commands/root.go index bbf7d6de..9435ce64 100644 --- a/commands/root.go +++ b/commands/root.go @@ -5,6 +5,7 @@ import ( "os" "github.com/MichaelMure/git-bug/bug" + "github.com/MichaelMure/git-bug/cache" "github.com/MichaelMure/git-bug/repository" "github.com/spf13/cobra" ) @@ -68,5 +69,13 @@ func loadRepo(cmd *cobra.Command, args []string) error { return err } + // Prevent the command from running when the cache has locked the repo + // Todo: make it more fine-grained at first + // Todo: make the running cache available for other processes + err = cache.RepoIsAvailable(repo) + if err != nil { + return err + } + return nil } diff --git a/commands/webui.go b/commands/webui.go index 9ebced09..f3f9c184 100644 --- a/commands/webui.go +++ b/commands/webui.go @@ -2,11 +2,14 @@ package commands import ( "bytes" + "context" "encoding/json" "fmt" "io/ioutil" "log" "net/http" + "os" + "os/signal" "time" "github.com/MichaelMure/git-bug/graphql" @@ -34,23 +37,66 @@ func runWebUI(cmd *cobra.Command, args []string) error { addr := fmt.Sprintf("127.0.0.1:%d", port) webUiAddr := fmt.Sprintf("http://%s", addr) - fmt.Printf("Web UI: %s\n", webUiAddr) - fmt.Printf("Graphql API: http://%s/graphql\n", addr) - fmt.Printf("Graphql Playground: http://%s/playground\n", addr) - router := mux.NewRouter() + graphqlHandler, err := graphql.NewHandler(repo) + if err != nil { + return err + } + // Routes router.Path("/playground").Handler(handler.Playground("git-bug", "/graphql")) - router.Path("/graphql").Handler(graphql.NewHandler(repo)) + router.Path("/graphql").Handler(graphqlHandler) router.Path("/gitfile/{hash}").Handler(newGitFileHandler(repo)) router.Path("/upload").Methods("POST").Handler(newGitUploadFileHandler(repo)) router.PathPrefix("/").Handler(http.FileServer(webui.WebUIAssets)) + srv := &http.Server{ + Addr: addr, + Handler: router, + } + + done := make(chan bool) + quit := make(chan os.Signal, 1) + + // register as handler of the interrupt signal to trigger the teardown + signal.Notify(quit, os.Interrupt) + + go func() { + <-quit + fmt.Println("WebUI is shutting down...") + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + srv.SetKeepAlivesEnabled(false) + if err := srv.Shutdown(ctx); err != nil { + log.Fatalf("Could not gracefully shutdown the WebUI: %v\n", err) + } + + // Teardown + err := graphqlHandler.Close() + if err != nil { + fmt.Println(err) + } + + close(done) + }() + + fmt.Printf("Web UI: %s\n", webUiAddr) + fmt.Printf("Graphql API: http://%s/graphql\n", addr) + fmt.Printf("Graphql Playground: http://%s/playground\n", addr) + open.Run(webUiAddr) - log.Fatal(http.ListenAndServe(addr, router)) + err = srv.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + return err + } + + <-done + fmt.Println("WebUI stopped") return nil } diff --git a/graphql/handler.go b/graphql/handler.go index 507cb508..b452d828 100644 --- a/graphql/handler.go +++ b/graphql/handler.go @@ -10,10 +10,22 @@ import ( "net/http" ) -func NewHandler(repo repository.Repo) http.Handler { - backend := resolvers.NewBackend() +type Handler struct { + http.HandlerFunc + *resolvers.Backend +} + +func NewHandler(repo repository.Repo) (Handler, error) { + h := Handler{ + Backend: resolvers.NewBackend(), + } + + err := h.Backend.RegisterDefaultRepository(repo) + if err != nil { + return Handler{}, err + } - backend.RegisterDefaultRepository(repo) + h.HandlerFunc = handler.GraphQL(graph.NewExecutableSchema(h.Backend)) - return handler.GraphQL(graph.NewExecutableSchema(backend)) + return h, nil } diff --git a/util/process.go b/util/process.go new file mode 100644 index 00000000..ddd3f704 --- /dev/null +++ b/util/process.go @@ -0,0 +1,23 @@ +package util + +import ( + "os" + "syscall" +) + +// ProcessIsRunning tell is a process is running +func ProcessIsRunning(pid int) bool { + // never return no error in a unix system + process, err := os.FindProcess(pid) + + if err != nil { + return false + } + + // Signal 0 doesn't do anything but allow testing the process + err = process.Signal(syscall.Signal(0)) + + // Todo: distinguish "you don't have access" and "process doesn't exist" + + return err == nil +} |