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