{"action":"created","comment":{"url":"https://api.github.com/repos/cozy/cozy-stack/pulls/comments/87990299","pull_request_review_id":8577550,"id":87990299,"diff_hunk":"@@ -0,0 +1,407 @@\n+package vfs\n+\n+import (\n+\t\"fmt\"\n+\t\"os\"\n+\t\"path\"\n+\t\"sync\"\n+\n+\t\"github.com/cozy/cozy-stack/couchdb\"\n+\t\"github.com/cozy/cozy-stack/couchdb/mango\"\n+\t\"github.com/cozy/cozy-stack/lru\"\n+\t\"golang.org/x/sync/errgroup\"\n+)\n+\n+// LocalCache implements the VFS Cache interface and should be used\n+// for mono-stack usage, where only one cozy-stack is accessing to the\n+// VFS at a time.\n+//\n+// Internally it provides some optimisations to cache file attributes\n+// and avoid having multiple useless RTTs with CouchDB.\n+type LocalCache struct {\n+\tmud  sync.RWMutex       // mutex for directories data-structures\n+\tlrud *lru.Cache         // lru cache for directories\n+\tpthd map[string]*string // path directory to id map\n+\n+\tmuf  sync.RWMutex       // mutex for files data-structures\n+\tlruf *lru.Cache         // lru cache for files\n+\tpthf map[string]*string // (folderID, name) file pair to id map\n+}\n+\n+// NewLocalCache creates an new LocalCache. The maxEntries parameter\n+// is used to specified the cache size: how many files and directories\n+// elements are kept in-memory\n+func NewLocalCache(maxEntries int) *LocalCache {\n+\tlc := new(LocalCache)\n+\tlc.init(maxEntries)\n+\treturn lc\n+}\n+\n+func (lc *LocalCache) init(maxEntries int) {\n+\tdirEviction := func(key string, value interface{}) {\n+\t\tif doc, ok := value.(*DirDoc); ok {\n+\t\t\tdelete(lc.pthd, doc.Fullpath)\n+\t\t}\n+\t}\n+\n+\tfileEviction := func(key string, value interface{}) {\n+\t\tif doc, ok := value.(*FileDoc); ok {\n+\t\t\tdelete(lc.pthf, genFilePathID(doc.FolderID, doc.Name))\n+\t\t}\n+\t}\n+\n+\tlc.pthd = make(map[string]*string)\n+\tlc.pthf = make(map[string]*string)\n+\tlc.lrud = &lru.Cache{MaxEntries: maxEntries, OnEvicted: dirEviction}\n+\tlc.lruf = &lru.Cache{MaxEntries: maxEntries, OnEvicted: fileEviction}\n+}\n+\n+// CreateDir is be used to persist a directory document\n+func (lc *LocalCache) CreateDir(c Context, doc *DirDoc) error {\n+\tvar err error\n+\tif err = doc.calcPath(c); err != nil {\n+\t\treturn err\n+\t}\n+\terr = couchdb.CreateDoc(c, doc)\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\tlc.touchDir(doc)\n+\treturn nil\n+}\n+\n+// UpdateDir is used to update a persisted directory document\n+func (lc *LocalCache) UpdateDir(c Context, olddoc, newdoc *DirDoc) error {\n+\toldpath, err := olddoc.Path(c)\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\n+\tif err = lc.updateDirDoc(c, newdoc); err != nil {\n+\t\treturn err\n+\t}\n+\n+\tnewpath, err := newdoc.Path(c)\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\n+\tif oldpath == newpath {\n+\t\treturn nil\n+\t}\n+\n+\tvar children []*DirDoc\n+\treq := &couchdb.FindRequest{\n+\t\tSelector: mango.StartWith(\"path\", oldpath+\"/\"),\n+\t}\n+\n+\terr = couchdb.FindDocs(c, FsDocType, req, &children)\n+\tif err != nil || len(children) == 0 {\n+\t\treturn err\n+\t}\n+\n+\tnewchildren := make([]*DirDoc, len(children))\n+\tfor i, child := range children {\n+\t\tnewchild, err := NewDirDoc(\n+\t\t\tchild.Name,\n+\t\t\tchild.FolderID,\n+\t\t\tchild.Tags,\n+\t\t)\n+\t\tif err != nil {\n+\t\t\treturn err\n+\t\t}\n+\n+\t\tnewchild.SetID(child.ID())\n+\t\tnewchild.SetRev(child.Rev())\n+\t\tnewchild.Fullpath = path.Join(newpath, child.Fullpath[len(oldpath)+1:])\n+\t\tnewchildren[i] = newchild\n+\t}\n+\n+\t// @TODO use couchdb bulk updates instead\n+\tvar g errgroup.Group\n+\tfor _, child := range newchildren {\n+\t\tnewchild := child\n+\t\tg.Go(func() error {\n+\t\t\treturn lc.updateDirDoc(c, newchild)\n+\t\t})\n+\t}\n+\treturn g.Wait()\n+}\n+\n+func (lc *LocalCache) updateDirDoc(c Context, doc *DirDoc) error {\n+\tif err := doc.calcPath(c); err != nil {\n+\t\treturn err\n+\t}\n+\tif err := couchdb.UpdateDoc(c, doc); err != nil {\n+\t\tlc.rmDir(doc)\n+\t\treturn err\n+\t}\n+\tlc.touchDir(doc)\n+\treturn nil\n+}\n+\n+// DirByID is used to fetch a directory given its ID\n+func (lc *LocalCache) DirByID(c Context, fileID string) (doc *DirDoc, err error) {\n+\tvar ok bool\n+\tif doc, ok = lc.dirCachedByID(fileID); ok {\n+\t\treturn\n+\t}\n+\n+\tdoc = &DirDoc{}\n+\terr = couchdb.GetDoc(c, FsDocType, fileID, doc)\n+\tif couchdb.IsNotFoundError(err) {\n+\t\terr = ErrParentDoesNotExist\n+\t} else if err == nil && doc.Type != DirType {\n+\t\terr = os.ErrNotExist\n+\t}\n+\tif err != nil {\n+\t\treturn\n+\t}\n+\n+\tlc.touchDir(doc)\n+\treturn\n+}\n+\n+// DirByPath is used to fetch a directory given its path\n+func (lc *LocalCache) DirByPath(c Context, name string) (doc *DirDoc, err error) {\n+\tvar ok bool\n+\tif doc, ok = lc.dirCachedByPath(name); ok {\n+\t\treturn\n+\t}\n+\n+\tvar docs []*DirDoc\n+\tsel := mango.Equal(\"path\", path.Clean(name))\n+\treq := &couchdb.FindRequest{Selector: sel, Limit: 1}\n+\terr = couchdb.FindDocs(c, FsDocType, req, &docs)\n+\tif err != nil {\n+\t\treturn nil, err\n+\t}\n+\tif len(docs) == 0 {\n+\t\treturn nil, os.ErrNotExist\n+\t}\n+\tdoc = docs[0]\n+\n+\tlc.touchDir(doc)\n+\treturn\n+}\n+\n+// DirFiles is used to fetch directory children (files and\n+// directories)\n+func (lc *LocalCache) DirFiles(c Context, doc *DirDoc) (files []*FileDoc, dirs []*DirDoc, err error) {\n+\tvar docs []*dirOrFile\n+\tsel := mango.Equal(\"folder_id\", doc.ID())\n+\treq := &couchdb.FindRequest{Selector: sel, Limit: 10}\n+\terr = couchdb.FindDocs(c, FsDocType, req, &docs)\n+\tif err != nil {\n+\t\treturn\n+\t}\n+\n+\tfor _, doc := range docs {\n+\t\ttyp, dir, file := doc.refine()\n+\t\tswitch typ {\n+\t\tcase FileType:\n+\t\t\tlc.touchFile(file)\n+\t\t\tfiles = append(files, file)\n+\t\tcase DirType:\n+\t\t\tlc.touchDir(dir)\n+\t\t\tdirs = append(dirs, dir)\n+\t\t}\n+\t}\n+\n+\treturn\n+}\n+\n+// CreateFile is be used to persist a file document\n+func (lc *LocalCache) CreateFile(c Context, doc *FileDoc) error {\n+\terr := couchdb.CreateDoc(c, doc)\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\tlc.touchFile(doc)\n+\treturn nil\n+}\n+\n+// UpdateFile is used to update a persisted file document\n+func (lc *LocalCache) UpdateFile(c Context, doc *FileDoc) error {\n+\terr := couchdb.UpdateDoc(c, doc)\n+\tif err != nil {\n+\t\tlc.rmFile(doc)\n+\t\treturn err\n+\t}\n+\tlc.touchFile(doc)\n+\treturn nil\n+}\n+\n+// FileByID is used to fetch a file given its ID\n+func (lc *LocalCache) FileByID(c Context, fileID string) (doc *FileDoc, err error) {\n+\tvar ok bool\n+\tif doc, ok = lc.fileCachedByID(fileID); ok {\n+\t\treturn\n+\t}\n+\n+\tdoc = &FileDoc{}\n+\terr = couchdb.GetDoc(c, FsDocType, fileID, doc)\n+\tif err != nil {\n+\t\treturn nil, err\n+\t}\n+\n+\tif doc.Type != FileType {\n+\t\treturn nil, os.ErrNotExist\n+\t}\n+\n+\treturn doc, nil\n+}\n+\n+// FileByPath is used to fetch a file given its path\n+func (lc *LocalCache) FileByPath(c Context, name string) (doc *FileDoc, err error) {\n+\tdirpath := path.Dir(name)\n+\tparent, err := lc.DirByPath(c, dirpath)\n+\tif err != nil {\n+\t\treturn\n+\t}\n+\n+\tfolderID, filename := parent.ID(), path.Base(name)\n+\n+\tvar ok bool\n+\tif doc, ok = lc.fileCachedByFolderID(folderID, filename); ok {\n+\t\treturn\n+\t}\n+\n+\tselector := mango.Map{\n+\t\t\"folder_id\": folderID,\n+\t\t\"name\":      path.Base(name),\n+\t\t\"type\":      FileType,\n+\t}\n+\n+\tvar docs []*FileDoc\n+\treq := &couchdb.FindRequest{\n+\t\tSelector: selector,\n+\t\tLimit:    1,\n+\t}\n+\terr = couchdb.FindDocs(c, FsDocType, req, &docs)\n+\tif err != nil {\n+\t\treturn\n+\t}\n+\tif len(docs) == 0 {\n+\t\terr = os.ErrNotExist\n+\t\treturn\n+\t}\n+\n+\tdoc = docs[0]\n+\tlc.touchFile(doc)\n+\treturn\n+}\n+\n+// DirOrFileByID is used to fetch a directory or file given its ID\n+func (lc *LocalCache) DirOrFileByID(c Context, fileID string) (typ string, dirDoc *DirDoc, fileDoc *FileDoc, err error) {\n+\tvar ok bool\n+\tif dirDoc, ok = lc.dirCachedByID(fileID); ok {\n+\t\ttyp = DirType\n+\t\treturn\n+\t}\n+\n+\tif fileDoc, ok = lc.fileCachedByID(fileID); ok {\n+\t\ttyp = FileType\n+\t\treturn\n+\t}\n+\n+\tdirOrFile := &dirOrFile{}\n+\terr = couchdb.GetDoc(c, FsDocType, fileID, dirOrFile)\n+\tif err != nil {\n+\t\treturn\n+\t}\n+\n+\ttyp, dirDoc, fileDoc = dirOrFile.refine()\n+\treturn\n+}\n+\n+// Len returns the total number of elements currently cached\n+func (lc *LocalCache) Len() int {\n+\tlc.mud.RLock()\n+\tlc.muf.RLock()\n+\tdefer lc.mud.RUnlock()\n+\tdefer lc.muf.RUnlock()\n+\treturn lc.lrud.Len() + lc.lruf.Len()\n+}\n+\n+func (lc *LocalCache) touchDir(doc *DirDoc) {","path":"vfs/cache_local.go","position":327,"original_position":327,"commit_id":"44fda990897d6f1e638b2c63c3e0c6396d108fc8","original_commit_id":"44fda990897d6f1e638b2c63c3e0c6396d108fc8","user":{"login":"nono","id":2767,"avatar_url":"https://avatars.githubusercontent.com/u/2767?v=3","gravatar_id":"","url":"https://api.github.com/users/nono","html_url":"https://github.com/nono","followers_url":"https://api.github.com/users/nono/followers","following_url":"https://api.github.com/users/nono/following{/other_user}","gists_url":"https://api.github.com/users/nono/gists{/gist_id}","starred_url":"https://api.github.com/users/nono/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/nono/subscriptions","organizations_url":"https://api.github.com/users/nono/orgs","repos_url":"https://api.github.com/users/nono/repos","events_url":"https://api.github.com/users/nono/events{/privacy}","received_events_url":"https://api.github.com/users/nono/received_events","type":"User","site_admin":false},"body":"They are both good. I have a small preference for `tapDir/tapFile`","created_at":"2016-11-15T10:44:10Z","updated_at":"2016-11-15T10:44:10Z","html_url":"https://github.com/cozy/cozy-stack/pull/69#discussion_r87990299","pull_request_url":"https://api.github.com/repos/cozy/cozy-stack/pulls/69","_links":{"self":{"href":"https://api.github.com/repos/cozy/cozy-stack/pulls/comments/87990299"},"html":{"href":"https://github.com/cozy/cozy-stack/pull/69#discussion_r87990299"},"pull_request":{"href":"https://api.github.com/repos/cozy/cozy-stack/pulls/69"}}},"pull_request":{"url":"https://api.github.com/repos/cozy/cozy-stack/pulls/69","id":92165992,"html_url":"https://github.com/cozy/cozy-stack/pull/69","diff_url":"https://github.com/cozy/cozy-stack/pull/69.diff","patch_url":"https://github.com/cozy/cozy-stack/pull/69.patch","issue_url":"https://api.github.com/repos/cozy/cozy-stack/issues/69","number":69,"state":"open","locked":false,"title":"[VFS] Add cache layer between vfs and couchdb","user":{"login":"jinroh","id":687847,"avatar_url":"https://avatars.githubusercontent.com/u/687847?v=3","gravatar_id":"","url":"https://api.github.com/users/jinroh","html_url":"https://github.com/jinroh","followers_url":"https://api.github.com/users/jinroh/followers","following_url":"https://api.github.com/users/jinroh/following{/other_user}","gists_url":"https://api.github.com/users/jinroh/gists{/gist_id}","starred_url":"https://api.github.com/users/jinroh/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jinroh/subscriptions","organizations_url":"https://api.github.com/users/jinroh/orgs","repos_url":"https://api.github.com/users/jinroh/repos","events_url":"https://api.github.com/users/jinroh/events{/privacy}","received_events_url":"https://api.github.com/users/jinroh/received_events","type":"User","site_admin":false},"body":"**~~Still work in progress~~, open for comments.**\r\n\r\nIn this PR I add a cache layer between the VFS and CouchDB.\r\n\r\nThis layer for now only does caching, it does not do any bulk operation. I measured from 3x to 5x amelioration when cloning a repository with 1000 files.\r\n\r\nSome parts of this cache layer could be generalised to any CouchDB access in our code, but for now it focuses on the VFS only.\r\n\r\nAs mentioned in #66, this cache would benefit from a rationalisation of our context system.","created_at":"2016-11-03T14:37:33Z","updated_at":"2016-11-15T10:44:10Z","closed_at":null,"merged_at":null,"merge_commit_sha":"a470a7d5bf489925a51ed19ce7812aa6756e8ea1","assignee":null,"assignees":[],"milestone":null,"commits_url":"https://api.github.com/repos/cozy/cozy-stack/pulls/69/commits","review_comments_url":"https://api.github.com/repos/cozy/cozy-stack/pulls/69/comments","review_comment_url":"https://api.github.com/repos/cozy/cozy-stack/pulls/comments{/number}","comments_url":"https://api.github.com/repos/cozy/cozy-stack/issues/69/comments","statuses_url":"https://api.github.com/repos/cozy/cozy-stack/statuses/44fda990897d6f1e638b2c63c3e0c6396d108fc8","head":{"label":"jinroh:vfs-lru-cache","ref":"vfs-lru-cache","sha":"44fda990897d6f1e638b2c63c3e0c6396d108fc8","user":{"login":"jinroh","id":687847,"avatar_url":"https://avatars.githubusercontent.com/u/687847?v=3","gravatar_id":"","url":"https://api.github.com/users/jinroh","html_url":"https://github.com/jinroh","followers_url":"https://api.github.com/users/jinroh/followers","following_url":"https://api.github.com/users/jinroh/following{/other_user}","gists_url":"https://api.github.com/users/jinroh/gists{/gist_id}","starred_url":"https://api.github.com/users/jinroh/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jinroh/subscriptions","organizations_url":"https://api.github.com/users/jinroh/orgs","repos_url":"https://api.github.com/users/jinroh/repos","events_url":"https://api.github.com/users/jinroh/events{/privacy}","received_events_url":"https://api.github.com/users/jinroh/received_events","type":"User","site_admin":false},"repo":{"id":70130658,"name":"cozy-stack","full_name":"jinroh/cozy-stack","owner":{"login":"jinroh","id":687847,"avatar_url":"https://avatars.githubusercontent.com/u/687847?v=3","gravatar_id":"","url":"https://api.github.com/users/jinroh","html_url":"https://github.com/jinroh","followers_url":"https://api.github.com/users/jinroh/followers","following_url":"https://api.github.com/users/jinroh/following{/other_user}","gists_url":"https://api.github.com/users/jinroh/gists{/gist_id}","starred_url":"https://api.github.com/users/jinroh/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jinroh/subscriptions","organizations_url":"https://api.github.com/users/jinroh/orgs","repos_url":"https://api.github.com/users/jinroh/repos","events_url":"https://api.github.com/users/jinroh/events{/privacy}","received_events_url":"https://api.github.com/users/jinroh/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/jinroh/cozy-stack","description":"Simple, Versatile, Yours","fork":true,"url":"https://api.github.com/repos/jinroh/cozy-stack","forks_url":"https://api.github.com/repos/jinroh/cozy-stack/forks","keys_url":"https://api.github.com/repos/jinroh/cozy-stack/keys{/key_id}","collaborators_url":"https://api.github.com/repos/jinroh/cozy-stack/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/jinroh/cozy-stack/teams","hooks_url":"https://api.github.com/repos/jinroh/cozy-stack/hooks","issue_events_url":"https://api.github.com/repos/jinroh/cozy-stack/issues/events{/number}","events_url":"https://api.github.com/repos/jinroh/cozy-stack/events","assignees_url":"https://api.github.com/repos/jinroh/cozy-stack/assignees{/user}","branches_url":"https://api.github.com/repos/jinroh/cozy-stack/branches{/branch}","tags_url":"https://api.github.com/repos/jinroh/cozy-stack/tags","blobs_url":"https://api.github.com/repos/jinroh/cozy-stack/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/jinroh/cozy-stack/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/jinroh/cozy-stack/git/refs{/sha}","trees_url":"https://api.github.com/repos/jinroh/cozy-stack/git/trees{/sha}","statuses_url":"https://api.github.com/repos/jinroh/cozy-stack/statuses/{sha}","languages_url":"https://api.github.com/repos/jinroh/cozy-stack/languages","stargazers_url":"https://api.github.com/repos/jinroh/cozy-stack/stargazers","contributors_url":"https://api.github.com/repos/jinroh/cozy-stack/contributors","subscribers_url":"https://api.github.com/repos/jinroh/cozy-stack/subscribers","subscription_url":"https://api.github.com/repos/jinroh/cozy-stack/subscription","commits_url":"https://api.github.com/repos/jinroh/cozy-stack/commits{/sha}","git_commits_url":"https://api.github.com/repos/jinroh/cozy-stack/git/commits{/sha}","comments_url":"https://api.github.com/repos/jinroh/cozy-stack/comments{/number}","issue_comment_url":"https://api.github.com/repos/jinroh/cozy-stack/issues/comments{/number}","contents_url":"https://api.github.com/repos/jinroh/cozy-stack/contents/{+path}","compare_url":"https://api.github.com/repos/jinroh/cozy-stack/compare/{base}...{head}","merges_url":"https://api.github.com/repos/jinroh/cozy-stack/merges","archive_url":"https://api.github.com/repos/jinroh/cozy-stack/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/jinroh/cozy-stack/downloads","issues_url":"https://api.github.com/repos/jinroh/cozy-stack/issues{/number}","pulls_url":"https://api.github.com/repos/jinroh/cozy-stack/pulls{/number}","milestones_url":"https://api.github.com/repos/jinroh/cozy-stack/milestones{/number}","notifications_url":"https://api.github.com/repos/jinroh/cozy-stack/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/jinroh/cozy-stack/labels{/name}","releases_url":"https://api.github.com/repos/jinroh/cozy-stack/releases{/id}","deployments_url":"https://api.github.com/repos/jinroh/cozy-stack/deployments","created_at":"2016-10-06T07:03:29Z","updated_at":"2016-10-06T07:03:30Z","pushed_at":"2016-11-15T09:21:24Z","git_url":"git://github.com/jinroh/cozy-stack.git","ssh_url":"git@github.com:jinroh/cozy-stack.git","clone_url":"https://github.com/jinroh/cozy-stack.git","svn_url":"https://github.com/jinroh/cozy-stack","homepage":"https://cozy.io","size":666,"stargazers_count":0,"watchers_count":0,"language":"Go","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"cozy:master","ref":"master","sha":"a7ed1ac05875dd44642807f5120058c794131a89","user":{"login":"cozy","id":7690525,"avatar_url":"https://avatars.githubusercontent.com/u/7690525?v=3","gravatar_id":"","url":"https://api.github.com/users/cozy","html_url":"https://github.com/cozy","followers_url":"https://api.github.com/users/cozy/followers","following_url":"https://api.github.com/users/cozy/following{/other_user}","gists_url":"https://api.github.com/users/cozy/gists{/gist_id}","starred_url":"https://api.github.com/users/cozy/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cozy/subscriptions","organizations_url":"https://api.github.com/users/cozy/orgs","repos_url":"https://api.github.com/users/cozy/repos","events_url":"https://api.github.com/users/cozy/events{/privacy}","received_events_url":"https://api.github.com/users/cozy/received_events","type":"Organization","site_admin":false},"repo":{"id":63433838,"name":"cozy-stack","full_name":"cozy/cozy-stack","owner":{"login":"cozy","id":7690525,"avatar_url":"https://avatars.githubusercontent.com/u/7690525?v=3","gravatar_id":"","url":"https://api.github.com/users/cozy","html_url":"https://github.com/cozy","followers_url":"https://api.github.com/users/cozy/followers","following_url":"https://api.github.com/users/cozy/following{/other_user}","gists_url":"https://api.github.com/users/cozy/gists{/gist_id}","starred_url":"https://api.github.com/users/cozy/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cozy/subscriptions","organizations_url":"https://api.github.com/users/cozy/orgs","repos_url":"https://api.github.com/users/cozy/repos","events_url":"https://api.github.com/users/cozy/events{/privacy}","received_events_url":"https://api.github.com/users/cozy/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/cozy/cozy-stack","description":"Simple, Versatile, Yours","fork":false,"url":"https://api.github.com/repos/cozy/cozy-stack","forks_url":"https://api.github.com/repos/cozy/cozy-stack/forks","keys_url":"https://api.github.com/repos/cozy/cozy-stack/keys{/key_id}","collaborators_url":"https://api.github.com/repos/cozy/cozy-stack/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/cozy/cozy-stack/teams","hooks_url":"https://api.github.com/repos/cozy/cozy-stack/hooks","issue_events_url":"https://api.github.com/repos/cozy/cozy-stack/issues/events{/number}","events_url":"https://api.github.com/repos/cozy/cozy-stack/events","assignees_url":"https://api.github.com/repos/cozy/cozy-stack/assignees{/user}","branches_url":"https://api.github.com/repos/cozy/cozy-stack/branches{/branch}","tags_url":"https://api.github.com/repos/cozy/cozy-stack/tags","blobs_url":"https://api.github.com/repos/cozy/cozy-stack/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/cozy/cozy-stack/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/cozy/cozy-stack/git/refs{/sha}","trees_url":"https://api.github.com/repos/cozy/cozy-stack/git/trees{/sha}","statuses_url":"https://api.github.com/repos/cozy/cozy-stack/statuses/{sha}","languages_url":"https://api.github.com/repos/cozy/cozy-stack/languages","stargazers_url":"https://api.github.com/repos/cozy/cozy-stack/stargazers","contributors_url":"https://api.github.com/repos/cozy/cozy-stack/contributors","subscribers_url":"https://api.github.com/repos/cozy/cozy-stack/subscribers","subscription_url":"https://api.github.com/repos/cozy/cozy-stack/subscription","commits_url":"https://api.github.com/repos/cozy/cozy-stack/commits{/sha}","git_commits_url":"https://api.github.com/repos/cozy/cozy-stack/git/commits{/sha}","comments_url":"https://api.github.com/repos/cozy/cozy-stack/comments{/number}","issue_comment_url":"https://api.github.com/repos/cozy/cozy-stack/issues/comments{/number}","contents_url":"https://api.github.com/repos/cozy/cozy-stack/contents/{+path}","compare_url":"https://api.github.com/repos/cozy/cozy-stack/compare/{base}...{head}","merges_url":"https://api.github.com/repos/cozy/cozy-stack/merges","archive_url":"https://api.github.com/repos/cozy/cozy-stack/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/cozy/cozy-stack/downloads","issues_url":"https://api.github.com/repos/cozy/cozy-stack/issues{/number}","pulls_url":"https://api.github.com/repos/cozy/cozy-stack/pulls{/number}","milestones_url":"https://api.github.com/repos/cozy/cozy-stack/milestones{/number}","notifications_url":"https://api.github.com/repos/cozy/cozy-stack/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/cozy/cozy-stack/labels{/name}","releases_url":"https://api.github.com/repos/cozy/cozy-stack/releases{/id}","deployments_url":"https://api.github.com/repos/cozy/cozy-stack/deployments","created_at":"2016-07-15T15:54:30Z","updated_at":"2016-11-03T14:58:00Z","pushed_at":"2016-11-15T10:10:09Z","git_url":"git://github.com/cozy/cozy-stack.git","ssh_url":"git@github.com:cozy/cozy-stack.git","clone_url":"https://github.com/cozy/cozy-stack.git","svn_url":"https://github.com/cozy/cozy-stack","homepage":"https://cozy.io","size":702,"stargazers_count":28,"watchers_count":28,"language":"Go","has_issues":true,"has_downloads":true,"has_wiki":false,"has_pages":true,"forks_count":10,"mirror_url":null,"open_issues_count":7,"forks":10,"open_issues":7,"watchers":28,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/cozy/cozy-stack/pulls/69"},"html":{"href":"https://github.com/cozy/cozy-stack/pull/69"},"issue":{"href":"https://api.github.com/repos/cozy/cozy-stack/issues/69"},"comments":{"href":"https://api.github.com/repos/cozy/cozy-stack/issues/69/comments"},"review_comments":{"href":"https://api.github.com/repos/cozy/cozy-stack/pulls/69/comments"},"review_comment":{"href":"https://api.github.com/repos/cozy/cozy-stack/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/cozy/cozy-stack/pulls/69/commits"},"statuses":{"href":"https://api.github.com/repos/cozy/cozy-stack/statuses/44fda990897d6f1e638b2c63c3e0c6396d108fc8"}}}}
{"actor":{"display_login":"nono"}}