{"action":"created","comment":{"url":"https://api.github.com/repos/coreos/etcd/pulls/comments/39471354","id":39471354,"diff_hunk":"@@ -0,0 +1,335 @@\n+package storage\n+\n+import (\n+\t\"errors\"\n+\t\"log\"\n+\t\"sync\"\n+\t\"time\"\n+\n+\t\"github.com/coreos/etcd/storage/storagepb\"\n+)\n+\n+// EOF is the error returned by Watcher.Err when watcher reaches its end revision and\n+// no more event is available.\n+var EOF = errors.New(\"EOF\")\n+\n+type watchableStore struct {\n+\tmu sync.Mutex\n+\n+\tKV\n+\n+\t// contains all unsynced watchers that needs to sync events that have happened\n+\t// TODO: use map to reduce cancel cost\n+\tunsynced []*watcher\n+\t// contains all synced watchers that are tracking the events that will happen\n+\t// The key of the map is the key that the watcher is watching on.\n+\tsynced map[string][]*watcher\n+\t// contains all synced watchers that have an end revision\n+\t// The key of the map is the end revision of the watcher.\n+\tendm map[int64][]*watcher\n+\ttx   *ongoingTx\n+\n+\tstopc chan struct{}\n+\twg    sync.WaitGroup\n+}\n+\n+func newWatchableStore(path string) *watchableStore {\n+\ts := &watchableStore{\n+\t\tKV:     newStore(path),\n+\t\tsynced: make(map[string][]*watcher),\n+\t\tendm:   make(map[int64][]*watcher),\n+\t\tstopc:  make(chan struct{}),\n+\t}\n+\ts.wg.Add(1)\n+\tgo s.keepSyncWatchers()\n+\treturn s\n+}\n+\n+func (s *watchableStore) Put(key, value []byte) (rev int64) {\n+\ts.mu.Lock()\n+\tdefer s.mu.Unlock()\n+\n+\trev = s.KV.Put(key, value)\n+\t// TODO: avoid this range\n+\tkvs, _, err := s.KV.Range(key, nil, 0, rev)\n+\tif err != nil {\n+\t\tlog.Panicf(\"unexpected range error (%v)\", err)\n+\t}\n+\ts.handle(rev, storagepb.Event{\n+\t\tType: storagepb.PUT,\n+\t\tKv:   &kvs[0],\n+\t})\n+\treturn rev\n+}\n+\n+func (s *watchableStore) DeleteRange(key, end []byte) (n, rev int64) {\n+\ts.mu.Lock()\n+\tdefer s.mu.Unlock()\n+\n+\t// TODO: avoid this range\n+\tkvs, _, err := s.KV.Range(key, end, 0, 0)\n+\tif err != nil {\n+\t\tlog.Panicf(\"unexpected range error (%v)\", err)\n+\t}\n+\tn, rev = s.KV.DeleteRange(key, end)\n+\tfor _, kv := range kvs {\n+\t\ts.handle(rev, storagepb.Event{\n+\t\t\tType: storagepb.DELETE,\n+\t\t\tKv: &storagepb.KeyValue{\n+\t\t\t\tKey: kv.Key,\n+\t\t\t},\n+\t\t})\n+\t}\n+\treturn n, rev\n+}\n+\n+func (s *watchableStore) TxnBegin() int64 {\n+\ts.mu.Lock()\n+\ts.tx = newOngoingTx()\n+\treturn s.KV.TxnBegin()\n+}\n+\n+func (s *watchableStore) TxnPut(txnID int64, key, value []byte) (rev int64, err error) {\n+\trev, err = s.KV.TxnPut(txnID, key, value)\n+\tif err == nil {\n+\t\ts.tx.put(string(key))\n+\t}\n+\treturn rev, err\n+}\n+\n+func (s *watchableStore) TxnDeleteRange(txnID int64, key, end []byte) (n, rev int64, err error) {\n+\tkvs, _, err := s.KV.TxnRange(txnID, key, end, 0, 0)\n+\tif err != nil {\n+\t\tlog.Panicf(\"unexpected range error (%v)\", err)\n+\t}\n+\tn, rev, err = s.KV.TxnDeleteRange(txnID, key, end)\n+\tif err == nil {\n+\t\tfor _, kv := range kvs {\n+\t\t\ts.tx.del(string(kv.Key))\n+\t\t}\n+\t}\n+\treturn n, rev, err\n+}\n+\n+func (s *watchableStore) TxnEnd(txnID int64) error {\n+\terr := s.KV.TxnEnd(txnID)\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\n+\t_, rev, _ := s.KV.Range(nil, nil, 0, 0)\n+\tfor k := range s.tx.putm {\n+\t\tkvs, _, err := s.KV.Range([]byte(k), nil, 0, 0)\n+\t\tif err != nil {\n+\t\t\tlog.Panicf(\"unexpected range error (%v)\", err)\n+\t\t}\n+\t\ts.handle(rev, storagepb.Event{\n+\t\t\tType: storagepb.PUT,\n+\t\t\tKv:   &kvs[0],\n+\t\t})\n+\t}\n+\tfor k := range s.tx.delm {\n+\t\ts.handle(rev, storagepb.Event{\n+\t\t\tType: storagepb.DELETE,\n+\t\t\tKv: &storagepb.KeyValue{\n+\t\t\t\tKey: []byte(k),\n+\t\t\t},\n+\t\t})\n+\t}\n+\ts.mu.Unlock()\n+\treturn nil\n+}\n+\n+func (s *watchableStore) Close() error {\n+\tclose(s.stopc)\n+\ts.wg.Wait()\n+\treturn s.KV.Close()\n+}\n+\n+func (s *watchableStore) Watcher(key []byte, prefix bool, startRev, endRev int64) (Watcher, CancelFunc) {\n+\ts.mu.Lock()\n+\tdefer s.mu.Unlock()\n+\n+\twa := newWatcher(key, prefix, startRev, endRev)\n+\tk := string(key)\n+\tif startRev == 0 {\n+\t\ts.synced[k] = append(s.synced[k], wa)\n+\t\tif endRev != 0 {\n+\t\t\ts.endm[endRev] = append(s.endm[endRev], wa)\n+\t\t}\n+\t} else {\n+\t\ts.unsynced = append(s.unsynced, wa)\n+\t}\n+\n+\tcancel := CancelFunc(func() {\n+\t\ts.mu.Lock()\n+\t\ts.mu.Unlock()\n+\t\twa.stopWithError(ErrCanceled)\n+\n+\t\t// remove global references of the watcher\n+\t\tfor i, w := range s.unsynced {\n+\t\t\tif w == wa {\n+\t\t\t\ts.unsynced = append(s.unsynced[:i], s.unsynced[i+1:]...)\n+\t\t\t\treturn\n+\t\t\t}\n+\t\t}\n+\n+\t\tfor i, w := range s.synced[k] {\n+\t\t\tif w == wa {\n+\t\t\t\ts.synced[k] = append(s.synced[k][:i], s.synced[k][i+1:]...)\n+\t\t\t}\n+\t\t}\n+\t\tif wa.end != 0 {\n+\t\t\tfor i, w := range s.endm[wa.end] {\n+\t\t\t\tif w == wa {\n+\t\t\t\t\ts.endm[wa.end] = append(s.endm[wa.end][:i], s.endm[wa.end][i+1:]...)\n+\t\t\t\t}\n+\t\t\t}\n+\t\t}\n+\t\t// If we cannot find it, it should have finished watch.\n+\t})\n+\n+\treturn wa, cancel\n+}\n+\n+// keepSyncWatchers syncs the watchers in the unsyncd map every 100ms.\n+func (s *watchableStore) keepSyncWatchers() {\n+\tdefer s.wg.Done()\n+\n+\tfor {\n+\t\ts.mu.Lock()\n+\t\ts.syncWatchers()\n+\t\ts.mu.Unlock()\n+\n+\t\tselect {\n+\t\tcase <-time.After(100 * time.Millisecond):\n+\t\tcase <-s.stopc:\n+\t\t\treturn\n+\t\t}\n+\t}\n+}\n+\n+// syncWatchers syncs the watchers in the unsyncd map.\n+func (s *watchableStore) syncWatchers() {\n+\t_, curRev, _ := s.KV.Range(nil, nil, 0, 0)\n+\n+\t// filtering without allocating\n+\t// https://github.com/golang/go/wiki/SliceTricks#filtering-without-allocating\n+\tnws := s.unsynced[:0]\n+\tfor _, w := range s.unsynced {\n+\t\tvar end []byte\n+\t\tif w.prefix {\n+\t\t\tend = make([]byte, len(w.key))\n+\t\t\tcopy(end, w.key)\n+\t\t\tend[len(w.key)-1]++\n+\t\t}\n+\t\tlimit := cap(w.ch) - len(w.ch)\n+\t\t// the channel is full, try it in the next round\n+\t\tif limit == 0 {\n+\t\t\tnws = append(nws, w)\n+\t\t\tcontinue\n+\t\t}\n+\t\tevs, nextRev, err := s.KV.(*store).RangeEvents(w.key, end, int64(limit), w.cur, w.end)\n+\t\tif err != nil {\n+\t\t\tw.stopWithError(err)\n+\t\t\tcontinue\n+\t\t}\n+\n+\t\t// push events to the channel\n+\t\tfor _, ev := range evs {\n+\t\t\tw.ch <- ev\n+\t\t}\n+\t\t// stop watcher if it reaches the end\n+\t\tif w.end > 0 && nextRev >= w.end {\n+\t\t\tw.stopWithError(EOF)\n+\t\t\tcontinue\n+\t\t}\n+\t\t// switch to tracking future events if needed\n+\t\tif nextRev > curRev {\n+\t\t\ts.synced[string(w.key)] = append(s.synced[string(w.key)], w)\n+\t\t\tif w.end != 0 {\n+\t\t\t\ts.endm[w.end] = append(s.endm[w.end], w)\n+\t\t\t}\n+\t\t\tcontinue\n+\t\t}\n+\t\t// put it back to try it in the next round\n+\t\tw.cur = nextRev\n+\t\tnws = append(nws, w)\n+\t}\n+\ts.unsynced = nws\n+}\n+\n+// handle handles the change of the happening event on all watchers.\n+func (s *watchableStore) handle(rev int64, ev storagepb.Event) {\n+\ts.notify(rev, ev)\n+\ts.stopWatchers(rev)\n+}\n+\n+// notify tells corresponded watchers that the given event just happened in the given rev.","path":"storage/watchable_store.go","position":268,"original_position":268,"commit_id":"7ca3d89883326fec98d0b693be556174e4a6c3da","original_commit_id":"7ca3d89883326fec98d0b693be556174e4a6c3da","user":{"login":"xiang90","id":4479947,"avatar_url":"https://avatars.githubusercontent.com/u/4479947?v=3","gravatar_id":"","url":"https://api.github.com/users/xiang90","html_url":"https://github.com/xiang90","followers_url":"https://api.github.com/users/xiang90/followers","following_url":"https://api.github.com/users/xiang90/following{/other_user}","gists_url":"https://api.github.com/users/xiang90/gists{/gist_id}","starred_url":"https://api.github.com/users/xiang90/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/xiang90/subscriptions","organizations_url":"https://api.github.com/users/xiang90/orgs","repos_url":"https://api.github.com/users/xiang90/repos","events_url":"https://api.github.com/users/xiang90/events{/privacy}","received_events_url":"https://api.github.com/users/xiang90/received_events","type":"User","site_admin":false},"body":"notify notifies watchers that...","created_at":"2015-09-15T03:27:07Z","updated_at":"2015-09-15T03:27:07Z","html_url":"https://github.com/coreos/etcd/pull/3507#discussion_r39471354","pull_request_url":"https://api.github.com/repos/coreos/etcd/pulls/3507","_links":{"self":{"href":"https://api.github.com/repos/coreos/etcd/pulls/comments/39471354"},"html":{"href":"https://github.com/coreos/etcd/pull/3507#discussion_r39471354"},"pull_request":{"href":"https://api.github.com/repos/coreos/etcd/pulls/3507"}}},"pull_request":{"url":"https://api.github.com/repos/coreos/etcd/pulls/3507","id":44864837,"html_url":"https://github.com/coreos/etcd/pull/3507","diff_url":"https://github.com/coreos/etcd/pull/3507.diff","patch_url":"https://github.com/coreos/etcd/pull/3507.patch","issue_url":"https://api.github.com/repos/coreos/etcd/issues/3507","number":3507,"state":"open","locked":false,"title":"storage: support basic watch","user":{"login":"yichengq","id":2976232,"avatar_url":"https://avatars.githubusercontent.com/u/2976232?v=3","gravatar_id":"","url":"https://api.github.com/users/yichengq","html_url":"https://github.com/yichengq","followers_url":"https://api.github.com/users/yichengq/followers","following_url":"https://api.github.com/users/yichengq/following{/other_user}","gists_url":"https://api.github.com/users/yichengq/gists{/gist_id}","starred_url":"https://api.github.com/users/yichengq/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yichengq/subscriptions","organizations_url":"https://api.github.com/users/yichengq/orgs","repos_url":"https://api.github.com/users/yichengq/repos","events_url":"https://api.github.com/users/yichengq/events{/privacy}","received_events_url":"https://api.github.com/users/yichengq/received_events","type":"User","site_admin":false},"body":"Each watch acclamis `446 B/op\t       6 allocs/op`. Watch uses fixed length-10 channel, and if httpHandler wants to cache more, it could cache events at its side.","created_at":"2015-09-13T01:29:44Z","updated_at":"2015-09-15T03:27:07Z","closed_at":null,"merged_at":null,"merge_commit_sha":"2820b442b3b111ae2a1e159c9feb951ff31e7ba2","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/coreos/etcd/pulls/3507/commits","review_comments_url":"https://api.github.com/repos/coreos/etcd/pulls/3507/comments","review_comment_url":"https://api.github.com/repos/coreos/etcd/pulls/comments{/number}","comments_url":"https://api.github.com/repos/coreos/etcd/issues/3507/comments","statuses_url":"https://api.github.com/repos/coreos/etcd/statuses/7ca3d89883326fec98d0b693be556174e4a6c3da","head":{"label":"yichengq:watch","ref":"watch","sha":"7ca3d89883326fec98d0b693be556174e4a6c3da","user":{"login":"yichengq","id":2976232,"avatar_url":"https://avatars.githubusercontent.com/u/2976232?v=3","gravatar_id":"","url":"https://api.github.com/users/yichengq","html_url":"https://github.com/yichengq","followers_url":"https://api.github.com/users/yichengq/followers","following_url":"https://api.github.com/users/yichengq/following{/other_user}","gists_url":"https://api.github.com/users/yichengq/gists{/gist_id}","starred_url":"https://api.github.com/users/yichengq/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yichengq/subscriptions","organizations_url":"https://api.github.com/users/yichengq/orgs","repos_url":"https://api.github.com/users/yichengq/repos","events_url":"https://api.github.com/users/yichengq/events{/privacy}","received_events_url":"https://api.github.com/users/yichengq/received_events","type":"User","site_admin":false},"repo":{"id":16744058,"name":"etcd","full_name":"yichengq/etcd","owner":{"login":"yichengq","id":2976232,"avatar_url":"https://avatars.githubusercontent.com/u/2976232?v=3","gravatar_id":"","url":"https://api.github.com/users/yichengq","html_url":"https://github.com/yichengq","followers_url":"https://api.github.com/users/yichengq/followers","following_url":"https://api.github.com/users/yichengq/following{/other_user}","gists_url":"https://api.github.com/users/yichengq/gists{/gist_id}","starred_url":"https://api.github.com/users/yichengq/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yichengq/subscriptions","organizations_url":"https://api.github.com/users/yichengq/orgs","repos_url":"https://api.github.com/users/yichengq/repos","events_url":"https://api.github.com/users/yichengq/events{/privacy}","received_events_url":"https://api.github.com/users/yichengq/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/yichengq/etcd","description":"A highly-available key value store for shared configuration and service discovery","fork":true,"url":"https://api.github.com/repos/yichengq/etcd","forks_url":"https://api.github.com/repos/yichengq/etcd/forks","keys_url":"https://api.github.com/repos/yichengq/etcd/keys{/key_id}","collaborators_url":"https://api.github.com/repos/yichengq/etcd/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/yichengq/etcd/teams","hooks_url":"https://api.github.com/repos/yichengq/etcd/hooks","issue_events_url":"https://api.github.com/repos/yichengq/etcd/issues/events{/number}","events_url":"https://api.github.com/repos/yichengq/etcd/events","assignees_url":"https://api.github.com/repos/yichengq/etcd/assignees{/user}","branches_url":"https://api.github.com/repos/yichengq/etcd/branches{/branch}","tags_url":"https://api.github.com/repos/yichengq/etcd/tags","blobs_url":"https://api.github.com/repos/yichengq/etcd/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/yichengq/etcd/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/yichengq/etcd/git/refs{/sha}","trees_url":"https://api.github.com/repos/yichengq/etcd/git/trees{/sha}","statuses_url":"https://api.github.com/repos/yichengq/etcd/statuses/{sha}","languages_url":"https://api.github.com/repos/yichengq/etcd/languages","stargazers_url":"https://api.github.com/repos/yichengq/etcd/stargazers","contributors_url":"https://api.github.com/repos/yichengq/etcd/contributors","subscribers_url":"https://api.github.com/repos/yichengq/etcd/subscribers","subscription_url":"https://api.github.com/repos/yichengq/etcd/subscription","commits_url":"https://api.github.com/repos/yichengq/etcd/commits{/sha}","git_commits_url":"https://api.github.com/repos/yichengq/etcd/git/commits{/sha}","comments_url":"https://api.github.com/repos/yichengq/etcd/comments{/number}","issue_comment_url":"https://api.github.com/repos/yichengq/etcd/issues/comments{/number}","contents_url":"https://api.github.com/repos/yichengq/etcd/contents/{+path}","compare_url":"https://api.github.com/repos/yichengq/etcd/compare/{base}...{head}","merges_url":"https://api.github.com/repos/yichengq/etcd/merges","archive_url":"https://api.github.com/repos/yichengq/etcd/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/yichengq/etcd/downloads","issues_url":"https://api.github.com/repos/yichengq/etcd/issues{/number}","pulls_url":"https://api.github.com/repos/yichengq/etcd/pulls{/number}","milestones_url":"https://api.github.com/repos/yichengq/etcd/milestones{/number}","notifications_url":"https://api.github.com/repos/yichengq/etcd/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/yichengq/etcd/labels{/name}","releases_url":"https://api.github.com/repos/yichengq/etcd/releases{/id}","created_at":"2014-02-11T20:18:23Z","updated_at":"2015-02-12T19:42:08Z","pushed_at":"2015-09-15T03:16:43Z","git_url":"git://github.com/yichengq/etcd.git","ssh_url":"git@github.com:yichengq/etcd.git","clone_url":"https://github.com/yichengq/etcd.git","svn_url":"https://github.com/yichengq/etcd","homepage":"","size":43005,"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":"coreos:master","ref":"master","sha":"b1c2d7e5265b559c7a88395753e151b618939dbb","user":{"login":"coreos","id":3730757,"avatar_url":"https://avatars.githubusercontent.com/u/3730757?v=3","gravatar_id":"","url":"https://api.github.com/users/coreos","html_url":"https://github.com/coreos","followers_url":"https://api.github.com/users/coreos/followers","following_url":"https://api.github.com/users/coreos/following{/other_user}","gists_url":"https://api.github.com/users/coreos/gists{/gist_id}","starred_url":"https://api.github.com/users/coreos/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/coreos/subscriptions","organizations_url":"https://api.github.com/users/coreos/orgs","repos_url":"https://api.github.com/users/coreos/repos","events_url":"https://api.github.com/users/coreos/events{/privacy}","received_events_url":"https://api.github.com/users/coreos/received_events","type":"Organization","site_admin":false},"repo":{"id":11225014,"name":"etcd","full_name":"coreos/etcd","owner":{"login":"coreos","id":3730757,"avatar_url":"https://avatars.githubusercontent.com/u/3730757?v=3","gravatar_id":"","url":"https://api.github.com/users/coreos","html_url":"https://github.com/coreos","followers_url":"https://api.github.com/users/coreos/followers","following_url":"https://api.github.com/users/coreos/following{/other_user}","gists_url":"https://api.github.com/users/coreos/gists{/gist_id}","starred_url":"https://api.github.com/users/coreos/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/coreos/subscriptions","organizations_url":"https://api.github.com/users/coreos/orgs","repos_url":"https://api.github.com/users/coreos/repos","events_url":"https://api.github.com/users/coreos/events{/privacy}","received_events_url":"https://api.github.com/users/coreos/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/coreos/etcd","description":"A distributed consistent key-value store for shared configuration and service discovery","fork":false,"url":"https://api.github.com/repos/coreos/etcd","forks_url":"https://api.github.com/repos/coreos/etcd/forks","keys_url":"https://api.github.com/repos/coreos/etcd/keys{/key_id}","collaborators_url":"https://api.github.com/repos/coreos/etcd/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/coreos/etcd/teams","hooks_url":"https://api.github.com/repos/coreos/etcd/hooks","issue_events_url":"https://api.github.com/repos/coreos/etcd/issues/events{/number}","events_url":"https://api.github.com/repos/coreos/etcd/events","assignees_url":"https://api.github.com/repos/coreos/etcd/assignees{/user}","branches_url":"https://api.github.com/repos/coreos/etcd/branches{/branch}","tags_url":"https://api.github.com/repos/coreos/etcd/tags","blobs_url":"https://api.github.com/repos/coreos/etcd/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/coreos/etcd/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/coreos/etcd/git/refs{/sha}","trees_url":"https://api.github.com/repos/coreos/etcd/git/trees{/sha}","statuses_url":"https://api.github.com/repos/coreos/etcd/statuses/{sha}","languages_url":"https://api.github.com/repos/coreos/etcd/languages","stargazers_url":"https://api.github.com/repos/coreos/etcd/stargazers","contributors_url":"https://api.github.com/repos/coreos/etcd/contributors","subscribers_url":"https://api.github.com/repos/coreos/etcd/subscribers","subscription_url":"https://api.github.com/repos/coreos/etcd/subscription","commits_url":"https://api.github.com/repos/coreos/etcd/commits{/sha}","git_commits_url":"https://api.github.com/repos/coreos/etcd/git/commits{/sha}","comments_url":"https://api.github.com/repos/coreos/etcd/comments{/number}","issue_comment_url":"https://api.github.com/repos/coreos/etcd/issues/comments{/number}","contents_url":"https://api.github.com/repos/coreos/etcd/contents/{+path}","compare_url":"https://api.github.com/repos/coreos/etcd/compare/{base}...{head}","merges_url":"https://api.github.com/repos/coreos/etcd/merges","archive_url":"https://api.github.com/repos/coreos/etcd/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/coreos/etcd/downloads","issues_url":"https://api.github.com/repos/coreos/etcd/issues{/number}","pulls_url":"https://api.github.com/repos/coreos/etcd/pulls{/number}","milestones_url":"https://api.github.com/repos/coreos/etcd/milestones{/number}","notifications_url":"https://api.github.com/repos/coreos/etcd/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/coreos/etcd/labels{/name}","releases_url":"https://api.github.com/repos/coreos/etcd/releases{/id}","created_at":"2013-07-06T21:57:21Z","updated_at":"2015-09-15T02:16:06Z","pushed_at":"2015-09-15T03:16:43Z","git_url":"git://github.com/coreos/etcd.git","ssh_url":"git@github.com:coreos/etcd.git","clone_url":"https://github.com/coreos/etcd.git","svn_url":"https://github.com/coreos/etcd","homepage":"","size":35947,"stargazers_count":7491,"watchers_count":7491,"language":"Go","has_issues":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":1094,"mirror_url":null,"open_issues_count":107,"forks":1094,"open_issues":107,"watchers":7491,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/coreos/etcd/pulls/3507"},"html":{"href":"https://github.com/coreos/etcd/pull/3507"},"issue":{"href":"https://api.github.com/repos/coreos/etcd/issues/3507"},"comments":{"href":"https://api.github.com/repos/coreos/etcd/issues/3507/comments"},"review_comments":{"href":"https://api.github.com/repos/coreos/etcd/pulls/3507/comments"},"review_comment":{"href":"https://api.github.com/repos/coreos/etcd/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/coreos/etcd/pulls/3507/commits"},"statuses":{"href":"https://api.github.com/repos/coreos/etcd/statuses/7ca3d89883326fec98d0b693be556174e4a6c3da"}}}}