cache.go

  1package cache
  2
  3import (
  4	"fmt"
  5	"io"
  6	"io/ioutil"
  7	"os"
  8	"path"
  9	"strconv"
 10
 11	"github.com/MichaelMure/git-bug/repository"
 12	"github.com/MichaelMure/git-bug/util"
 13)
 14
 15const lockfile = "lock"
 16const excerptsFile = "excerpts"
 17
 18type RootCache struct {
 19	repos map[string]*RepoCache
 20}
 21
 22func NewCache() RootCache {
 23	return RootCache{
 24		repos: make(map[string]*RepoCache),
 25	}
 26}
 27
 28// RegisterRepository register a named repository. Use this for multi-repo setup
 29func (c *RootCache) RegisterRepository(ref string, repo repository.Repo) error {
 30	err := c.lockRepository(repo)
 31	if err != nil {
 32		return err
 33	}
 34
 35	r, err := NewRepoCache(repo)
 36	if err != nil {
 37		return err
 38	}
 39
 40	c.repos[ref] = r
 41	return nil
 42}
 43
 44// RegisterDefaultRepository register a unnamed repository. Use this for mono-repo setup
 45func (c *RootCache) RegisterDefaultRepository(repo repository.Repo) error {
 46	err := c.lockRepository(repo)
 47	if err != nil {
 48		return err
 49	}
 50
 51	r, err := NewRepoCache(repo)
 52	if err != nil {
 53		return err
 54	}
 55
 56	c.repos[""] = r
 57	return nil
 58}
 59
 60func (c *RootCache) lockRepository(repo repository.Repo) error {
 61	lockPath := repoLockFilePath(repo)
 62
 63	err := RepoIsAvailable(repo)
 64	if err != nil {
 65		return err
 66	}
 67
 68	f, err := os.Create(lockPath)
 69	if err != nil {
 70		return err
 71	}
 72
 73	pid := fmt.Sprintf("%d", os.Getpid())
 74	_, err = f.WriteString(pid)
 75	if err != nil {
 76		return err
 77	}
 78
 79	return f.Close()
 80}
 81
 82// ResolveRepo retrieve a repository by name
 83func (c *RootCache) DefaultRepo() (*RepoCache, error) {
 84	if len(c.repos) != 1 {
 85		return nil, fmt.Errorf("repository is not unique")
 86	}
 87
 88	for _, r := range c.repos {
 89		return r, nil
 90	}
 91
 92	panic("unreachable")
 93}
 94
 95// DefaultRepo retrieve the default repository
 96func (c *RootCache) ResolveRepo(ref string) (*RepoCache, error) {
 97	r, ok := c.repos[ref]
 98	if !ok {
 99		return nil, fmt.Errorf("unknown repo")
100	}
101	return r, nil
102}
103
104// Close will do anything that is needed to close the cache properly
105func (c *RootCache) Close() error {
106	for _, cachedRepo := range c.repos {
107		lockPath := repoLockFilePath(cachedRepo.repo)
108		err := os.Remove(lockPath)
109		if err != nil {
110			return err
111		}
112	}
113	return nil
114}
115
116// RepoIsAvailable check is the given repository is locked by a Cache.
117// Note: this is a smart function that will cleanup the lock file if the
118// corresponding process is not there anymore.
119// If no error is returned, the repo is free to edit.
120func RepoIsAvailable(repo repository.Repo) error {
121	lockPath := repoLockFilePath(repo)
122
123	// Todo: this leave way for a racey access to the repo between the test
124	// if the file exist and the actual write. It's probably not a problem in
125	// practice because using a repository will be done from user interaction
126	// or in a context where a single instance of git-bug is already guaranteed
127	// (say, a server with the web UI running). But still, that might be nice to
128	// have a mutex or something to guard that.
129
130	// Todo: this will fail if somehow the filesystem is shared with another
131	// computer. Should add a configuration that prevent the cleaning of the
132	// lock file
133
134	f, err := os.Open(lockPath)
135
136	if err != nil && !os.IsNotExist(err) {
137		return err
138	}
139
140	if err == nil {
141		// lock file already exist
142		buf, err := ioutil.ReadAll(io.LimitReader(f, 10))
143		if err != nil {
144			return err
145		}
146		if len(buf) == 10 {
147			return fmt.Errorf("The lock file should be < 10 bytes")
148		}
149
150		pid, err := strconv.Atoi(string(buf))
151		if err != nil {
152			return err
153		}
154
155		if util.ProcessIsRunning(pid) {
156			return fmt.Errorf("The repository you want to access is already locked by the process pid %d", pid)
157		}
158
159		// The lock file is just laying there after a crash, clean it
160
161		fmt.Println("A lock file is present but the corresponding process is not, removing it.")
162		err = f.Close()
163		if err != nil {
164			return err
165		}
166
167		os.Remove(lockPath)
168		if err != nil {
169			return err
170		}
171	}
172
173	return nil
174}
175
176func repoLockFilePath(repo repository.Repo) string {
177	return path.Join(repo.GetPath(), ".git", "git-bug", lockfile)
178}