{"action":"created","comment":{"url":"https://api.github.com/repos/line/centraldogma/pulls/comments/185686750","pull_request_review_id":117125031,"id":185686750,"diff_hunk":"@@ -0,0 +1,378 @@\n+// Copyright 2018 LINE Corporation\n+//\n+// LINE Corporation licenses this file to you under the Apache License,\n+// version 2.0 (the \"License\"); you may not use this file except in compliance\n+// with the License. You may obtain a copy of the License at:\n+//\n+//   https://www.apache.org/licenses/LICENSE-2.0\n+//\n+// Unless required by applicable law or agreed to in writing, software\n+// distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n+// License for the specific language governing permissions and limitations\n+// under the License.\n+\n+package dogma\n+\n+import (\n+\t\"context\"\n+\t\"errors\"\n+\t\"fmt\"\n+\t\"math/rand\"\n+\t\"net/http\"\n+\t\"net/url\"\n+\t\"strconv\"\n+\t\"strings\"\n+\t\"sync\"\n+\t\"sync/atomic\"\n+\t\"time\"\n+\t\"unsafe\"\n+)\n+\n+type watchService service\n+\n+// WatchResult represents a result from watch operation.\n+type WatchResult struct {\n+\tCommit *Commit\n+\tRes    *http.Response\n+\tErr    error\n+}\n+\n+func (ws *watchService) watchFile(ctx context.Context, projectName, repoName, lastKnownRevision string,\n+\tquery *Query, timeout time.Duration) <-chan *WatchResult {\n+\twatchResult := make(chan *WatchResult)\n+\tif query == nil {\n+\t\twatchResult <- &WatchResult{Err: errors.New(\"query should not be nil\")}\n+\t\treturn watchResult\n+\t}\n+\n+\tu := fmt.Sprintf(\"%vprojects/%v/repos/%v/contents%v\", defaultPathPrefix, projectName, repoName, query.Path)\n+\tv := &url.Values{}\n+\tif query != nil && query.Type == JSONPath {\n+\t\tif err := setJSONPaths(v, query.Path, query.Expressions); err != nil {\n+\t\t\twatchResult <- &WatchResult{Err: err}\n+\t\t\treturn watchResult\n+\t\t}\n+\t}\n+\tu += encodeValues(v)\n+\tws.watchRequest(ctx, watchResult, u, lastKnownRevision, timeout)\n+\treturn watchResult\n+}\n+\n+func (ws *watchService) watchRepo(ctx context.Context,\n+\tprojectName, repoName, lastKnownRevision, pathPattern string, timeout time.Duration) <-chan *WatchResult {\n+\twatchResult := make(chan *WatchResult)\n+\tif len(pathPattern) != 0 && !strings.HasPrefix(pathPattern, \"/\") {\n+\t\t// Normalize the pathPattern when it does not start with \"/\" so that the pathPattern fits into the url.\n+\t\tpathPattern = \"/**/\" + pathPattern\n+\t}\n+\n+\tu := fmt.Sprintf(\"%vprojects/%v/repos/%v/contents%v\", defaultPathPrefix, projectName, repoName, pathPattern)\n+\tws.watchRequest(ctx, watchResult, u, lastKnownRevision, timeout)\n+\treturn watchResult\n+}\n+\n+func (ws *watchService) watchRequest(ctx context.Context, watchResult chan<- *WatchResult,\n+\tu, lastKnownRevision string, timeout time.Duration) {\n+\treq, err := ws.client.newRequest(\"GET\", u, nil)\n+\tif err != nil {\n+\t\twatchResult <- &WatchResult{Err: err}\n+\t\treturn\n+\t}\n+\tif len(lastKnownRevision) != 0 {\n+\t\treq.Header.Set(\"if-none-match\", lastKnownRevision)\n+\t} else {\n+\t\treq.Header.Set(\"if-none-match\", \"-1\")\n+\t}\n+\tif timeout != 0 {\n+\t\treq.Header.Set(\"prefer\", fmt.Sprintf(\"wait=%v\", timeout.Seconds()))\n+\t}\n+\n+\tgo func() {\n+\t\tcommit := new(Commit)\n+\t\tres, err := ws.client.do(ctx, req, commit)\n+\t\tif err != nil {\n+\t\t\twatchResult <- &WatchResult{Commit: nil, Res: res, Err: err}\n+\t\t} else {\n+\t\t\twatchResult <- &WatchResult{Commit: commit, Res: res, Err: nil}\n+\t\t}\n+\t}()\n+}\n+\n+const (\n+\tdelayOnSuccess = 1 * time.Second\n+\tminInterval    = delayOnSuccess * 2\n+\tmaxInterval    = 1 * time.Minute\n+\tjitterRate     = 0.2\n+\twatchTimeout   = 1 * time.Minute\n+\tmaxInt63       = int64(^uint64(0) >> 1)\n+)\n+\n+// These constants represent the state of a watcher.\n+const (\n+\tinitial int32 = iota\n+\tstarted\n+\tstopped\n+)\n+\n+// Watcher watches the changes of a repository or a file.\n+type Watcher struct {\n+\tstate              int32\n+\tinitialChan        chan *Latest // channel whose buffer is 1.\n+\tscheduleCTX        context.Context\n+\tscheduleCancelFunc func()\n+\twatchCTX           context.Context\n+\twatchCancelFunc    func()\n+\tlatestPtr          unsafe.Pointer\n+\tupdateListeners    []func(revision int, value interface{})\n+\tlistenersMutex     *sync.Mutex\n+\n+\tdoWatchFunc          func(lastKnownRevision int) <-chan *WatchResult\n+\tconvertingResultFunc func(result *WatchResult) *Latest\n+\n+\tprojectName string\n+\trepoName    string\n+\tpathPattern string\n+}\n+\n+// Latest represents a holder of the latest known value and its Revision retrieved by Watcher.\n+type Latest struct {\n+\tRevision int\n+\tValue    interface{}\n+\tErr      error\n+}\n+\n+func newWatcher(projectName, repoName, pathPattern string) *Watcher {\n+\trand.Seed(time.Now().UTC().UnixNano())\n+\tscheduleCTX, scheduleCancelFunc := context.WithCancel(context.Background())\n+\twatchCTX, watchCancelFunc := context.WithCancel(context.Background())\n+\treturn &Watcher{state: initial, initialChan: make(chan *Latest, 2),\n+\t\tscheduleCTX: scheduleCTX, scheduleCancelFunc: scheduleCancelFunc,\n+\t\twatchCTX: watchCTX, watchCancelFunc: watchCancelFunc,\n+\t\tlistenersMutex: &sync.Mutex{},\n+\t\tprojectName:    projectName,\n+\t\trepoName:       repoName, pathPattern: pathPattern}\n+}\n+\n+// AwaitInitialValue awaits for the initial value to be available.\n+func (w *Watcher) AwaitInitialValue() Latest {\n+\tlatest := <-w.initialChan\n+\t// Put it back to the channel so that this can return the value multiple times.\n+\tw.initialChan <- latest\n+\treturn *latest\n+}\n+\n+// AwaitInitialValue awaits for the initial value to be available during the specified timeout.\n+func (w *Watcher) AwaitInitialValueWith(timeout time.Duration) Latest {\n+\tselect {\n+\tcase latest := <-w.initialChan:\n+\t\t// Put it back to the channel so that this can return the value multiple times.\n+\t\tw.initialChan <- latest\n+\t\treturn *latest\n+\tcase <-time.After(timeout):\n+\t\treturn Latest{Err: fmt.Errorf(\"failed to get the initial value. timeout: %v\", timeout)}\n+\t}\n+}\n+\n+// Latest returns the latest Revision and value of WatchFile() or WatchRepository() result.\n+func (w *Watcher) Latest() Latest {\n+\tif latest := (*Latest)(atomic.LoadPointer(&w.latestPtr)); latest != nil {\n+\t\treturn *latest\n+\t} else {\n+\t\treturn Latest{Err: errors.New(\"latest is not set yet\")}\n+\t}\n+}\n+\n+// LatestValue returns the latest value of watchFile() or WatchRepository() result.\n+func (w *Watcher) LatestValue() (interface{}, error) {\n+\tlatest := w.Latest()\n+\tif latest.Err != nil {\n+\t\treturn nil, latest.Err\n+\t}\n+\treturn latest.Value, nil\n+}\n+\n+// LatestValue returns the latest value of watchFile() or WatchRepository() result. If it's not available, this\n+// returns the defaultValue.\n+func (w *Watcher) LatestValueOr(defaultValue interface{}) interface{} {\n+\tlatest := w.Latest()\n+\tif latest.Err != nil {\n+\t\treturn defaultValue\n+\t}\n+\treturn latest.Value\n+}\n+\n+// Close stops watching the file specified in the Query or the pathPattern in the repository.\n+func (w *Watcher) Close() {\n+\tatomic.StoreInt32(&w.state, stopped)\n+\tselect {\n+\tcase latest := <-w.initialChan:\n+\t\tw.initialChan <- latest\n+\tcase <-time.After(100 * time.Millisecond):\n+\t\tw.initialChan <- &Latest{Err: errors.New(\"watcher is closed\")}\n+\t}\n+\tw.scheduleCancelFunc() // After the first call, subsequent calls to a CancelFunc do nothing.\n+\tw.watchCancelFunc()\n+}\n+\n+// Watch registers a func that will be invoked when the value of the watched entry becomes available or changes.\n+func (w *Watcher) Watch(listener func(revision int, value interface{})) error {\n+\tif listener == nil {\n+\t\treturn errors.New(\"listener is nil\")\n+\t}\n+\tif w.isStopped() {\n+\t\treturn errors.New(\"watcher is closed\")\n+\t}\n+\n+\tw.listenersMutex.Lock()\n+\tdefer w.listenersMutex.Unlock()\n+\tw.updateListeners = append(w.updateListeners, listener)\n+\n+\tif latest := w.Latest(); latest.Err == nil {\n+\t\tgo func() {\n+\t\t\t// Perform initial notification so that the listener always gets the initial value.\n+\t\t\tlistener(latest.Revision, latest.Value)\n+\t\t}()\n+\t}\n+\n+\treturn nil\n+}\n+\n+func (ws *watchService) fileWatcher(projectName, repoName string, query *Query,\n+\tconvertingValueFunc func(value interface{}) interface{}) (*Watcher, error) {\n+\tif query == nil {\n+\t\treturn nil, errors.New(\"query should not be nil\")\n+\t}\n+\n+\tw := newWatcher(projectName, repoName, query.Path)\n+\tw.doWatchFunc = func(lastKnownRevision int) <-chan *WatchResult {\n+\t\treturn ws.watchFile(w.watchCTX, projectName, repoName, strconv.Itoa(lastKnownRevision),\n+\t\t\tquery, watchTimeout)\n+\t}\n+\tw.convertingResultFunc = func(result *WatchResult) *Latest {\n+\t\tvar value interface{}\n+\t\tif convertingValueFunc != nil {\n+\t\t\tvalue = convertingValueFunc(result.Commit.Entries[0].Content)\n+\t\t} else {\n+\t\t\tvalue = result.Commit.Entries[0].Content\n+\t\t}\n+\n+\t\treturn &Latest{Revision: result.Commit.Revision, Value: value}\n+\t}\n+\treturn w, nil\n+}\n+\n+func (ws *watchService) repoWatcher(projectName, repoName, pathPattern string,\n+\tconvertingValueFunc func(revision int) interface{}) (*Watcher, error) {\n+\tw := newWatcher(projectName, repoName, pathPattern)\n+\tw.doWatchFunc = func(lastKnownRevision int) <-chan *WatchResult {\n+\t\treturn ws.watchRepo(w.watchCTX, projectName, repoName, strconv.Itoa(lastKnownRevision),\n+\t\t\tpathPattern, watchTimeout)\n+\t}\n+\tw.convertingResultFunc = func(result *WatchResult) *Latest {\n+\t\tvar value interface{}\n+\t\trevision := result.Commit.Revision\n+\t\tif convertingValueFunc != nil {\n+\t\t\tvalue = convertingValueFunc(revision)\n+\t\t} else {\n+\t\t\tvalue = revision\n+\t\t}\n+\n+\t\treturn &Latest{Revision: revision, Value: value}\n+\t}\n+\treturn w, nil\n+}\n+\n+func (w *Watcher) start() {\n+\tif atomic.CompareAndSwapInt32(&w.state, initial, started) {\n+\t\tw.scheduleWatch(0)\n+\t}\n+}\n+\n+func (w *Watcher) scheduleWatch(numAttemptsSoFar int) {\n+\tif w.isStopped() {\n+\t\treturn\n+\t}\n+\n+\tvar delay time.Duration\n+\tif numAttemptsSoFar == 0 {\n+\t\tlatest := w.Latest()\n+\t\tif latest.Err != nil {\n+\t\t\tdelay = delayOnSuccess\n+\t\t} else {\n+\t\t\tdelay = 0\n+\t\t}\n+\t} else {\n+\t\tdelay = nextDelay(numAttemptsSoFar)\n+\t}\n+\n+\tgo func() {\n+\t\tselect {\n+\t\tcase <-w.scheduleCTX.Done():\n+\t\tcase <-time.NewTimer(delay).C:\n+\t\t\tw.doWatch(numAttemptsSoFar)\n+\t\t}\n+\t}()\n+}\n+\n+func (w *Watcher) isStopped() bool {\n+\tstate := atomic.LoadInt32(&w.state)\n+\treturn state == stopped\n+}\n+\n+func (w *Watcher) doWatch(numAttemptsSoFar int) {\n+\tif w.isStopped() {\n+\t\treturn\n+\t}\n+\n+\tvar lastKnownRevision int\n+\tcurLatest := w.Latest()\n+\tif curLatest.Revision == 0 {\n+\t\tlastKnownRevision = 1 // Init revision\n+\t} else {\n+\t\tlastKnownRevision = curLatest.Revision\n+\t}\n+\n+\tselect {\n+\tcase <-w.watchCTX.Done():\n+\tcase watchResult := <-w.doWatchFunc(lastKnownRevision):\n+\t\tif watchResult.Err != nil {\n+\t\t\tif watchResult.Err == context.Canceled {\n+\t\t\t\t// Cancelled by close()\n+\t\t\t\treturn\n+\t\t\t}\n+\n+\t\t\tlog.Debug(watchResult.Err)\n+\t\t\tw.scheduleWatch(numAttemptsSoFar + 1)\n+\t\t\treturn\n+\t\t}\n+\n+\t\tnewLatest := w.convertingResultFunc(watchResult)\n+\t\tif oldLatest := w.Latest(); oldLatest.Err != nil {\n+\t\t\t// latest is not set yet\n+\t\t\tw.initialChan <- newLatest\n+\t\t}\n+\t\tatomic.StorePointer(&w.latestPtr, (unsafe.Pointer)(newLatest))\n+\t\tlog.Debugf(\"Watcher noticed updated file %s/%s%s: rev=%v\",\n+\t\t\tw.projectName, w.repoName, w.pathPattern, newLatest.Revision)\n+\t\tw.notifyListeners()\n+\t\tw.scheduleWatch(0)\n+\t}\n+}\n+\n+func (w *Watcher) notifyListeners() {\n+\tif w.isStopped() {\n+\t\t// Do not notify after stopped.\n+\t\treturn\n+\t}\n+\n+\tlatest := w.Latest()\n+\tw.listenersMutex.Lock()\n+\tdefer w.listenersMutex.Unlock()\n+\n+\tgo func() {\n+\t\tfor _, listener := range w.updateListeners {","path":"client/go/dogma/watch_service.go","position":374,"original_position":374,"commit_id":"6ecbac6afdfc47dfe63225b893cf54e6a4c9507e","original_commit_id":"6ecbac6afdfc47dfe63225b893cf54e6a4c9507e","user":{"login":"minwoox","id":25103250,"avatar_url":"https://avatars2.githubusercontent.com/u/25103250?v=4","gravatar_id":"","url":"https://api.github.com/users/minwoox","html_url":"https://github.com/minwoox","followers_url":"https://api.github.com/users/minwoox/followers","following_url":"https://api.github.com/users/minwoox/following{/other_user}","gists_url":"https://api.github.com/users/minwoox/gists{/gist_id}","starred_url":"https://api.github.com/users/minwoox/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/minwoox/subscriptions","organizations_url":"https://api.github.com/users/minwoox/orgs","repos_url":"https://api.github.com/users/minwoox/repos","events_url":"https://api.github.com/users/minwoox/events{/privacy}","received_events_url":"https://api.github.com/users/minwoox/received_events","type":"User","site_admin":false},"body":"Oops I will make a copy of it. Originally there was no Goroutine that executes the `listeners`.\r\nBut I thought that using Goroutine here is better in case something happens while executing `listerens` such as long time job.\r\nSo I put it and forgot to make a copy of it. :-)","created_at":"2018-05-03T02:27:41Z","updated_at":"2018-05-03T02:27:41Z","html_url":"https://github.com/line/centraldogma/pull/207#discussion_r185686750","pull_request_url":"https://api.github.com/repos/line/centraldogma/pulls/207","author_association":"MEMBER","_links":{"self":{"href":"https://api.github.com/repos/line/centraldogma/pulls/comments/185686750"},"html":{"href":"https://github.com/line/centraldogma/pull/207#discussion_r185686750"},"pull_request":{"href":"https://api.github.com/repos/line/centraldogma/pulls/207"}},"in_reply_to_id":185678023},"pull_request":{"url":"https://api.github.com/repos/line/centraldogma/pulls/207","id":185427417,"html_url":"https://github.com/line/centraldogma/pull/207","diff_url":"https://github.com/line/centraldogma/pull/207.diff","patch_url":"https://github.com/line/centraldogma/pull/207.patch","issue_url":"https://api.github.com/repos/line/centraldogma/issues/207","number":207,"state":"open","locked":false,"title":"Add watcher to Go library","user":{"login":"minwoox","id":25103250,"avatar_url":"https://avatars2.githubusercontent.com/u/25103250?v=4","gravatar_id":"","url":"https://api.github.com/users/minwoox","html_url":"https://github.com/minwoox","followers_url":"https://api.github.com/users/minwoox/followers","following_url":"https://api.github.com/users/minwoox/following{/other_user}","gists_url":"https://api.github.com/users/minwoox/gists{/gist_id}","starred_url":"https://api.github.com/users/minwoox/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/minwoox/subscriptions","organizations_url":"https://api.github.com/users/minwoox/orgs","repos_url":"https://api.github.com/users/minwoox/repos","events_url":"https://api.github.com/users/minwoox/events{/privacy}","received_events_url":"https://api.github.com/users/minwoox/received_events","type":"User","site_admin":false},"body":"Motivation:\r\nIf we provide `watcher` in Go library, a user can utilize Central Dogma more.\r\n\r\nModifications:\r\n- Add `Watcher` to Go library\r\n- Add github.com/sirupsen/logrus/ in order to log by the level\r\n\r\nResult:\r\n- Better usage of Go library","created_at":"2018-05-02T12:47:04Z","updated_at":"2018-05-03T02:27:41Z","closed_at":null,"merged_at":null,"merge_commit_sha":"84bc53a0b57e07b1c8eb1aa72186b2d1e1811391","assignee":null,"assignees":[],"requested_reviewers":[{"login":"hyangtack","id":8417787,"avatar_url":"https://avatars1.githubusercontent.com/u/8417787?v=4","gravatar_id":"","url":"https://api.github.com/users/hyangtack","html_url":"https://github.com/hyangtack","followers_url":"https://api.github.com/users/hyangtack/followers","following_url":"https://api.github.com/users/hyangtack/following{/other_user}","gists_url":"https://api.github.com/users/hyangtack/gists{/gist_id}","starred_url":"https://api.github.com/users/hyangtack/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/hyangtack/subscriptions","organizations_url":"https://api.github.com/users/hyangtack/orgs","repos_url":"https://api.github.com/users/hyangtack/repos","events_url":"https://api.github.com/users/hyangtack/events{/privacy}","received_events_url":"https://api.github.com/users/hyangtack/received_events","type":"User","site_admin":false}],"requested_teams":[],"labels":[{"id":716399577,"url":"https://api.github.com/repos/line/centraldogma/labels/new-feature","name":"new-feature","color":"009800","default":false}],"milestone":{"url":"https://api.github.com/repos/line/centraldogma/milestones/10","html_url":"https://github.com/line/centraldogma/milestone/10","labels_url":"https://api.github.com/repos/line/centraldogma/milestones/10/labels","id":3316596,"number":10,"title":"0.25.0","description":null,"creator":{"login":"trustin","id":173918,"avatar_url":"https://avatars0.githubusercontent.com/u/173918?v=4","gravatar_id":"","url":"https://api.github.com/users/trustin","html_url":"https://github.com/trustin","followers_url":"https://api.github.com/users/trustin/followers","following_url":"https://api.github.com/users/trustin/following{/other_user}","gists_url":"https://api.github.com/users/trustin/gists{/gist_id}","starred_url":"https://api.github.com/users/trustin/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/trustin/subscriptions","organizations_url":"https://api.github.com/users/trustin/orgs","repos_url":"https://api.github.com/users/trustin/repos","events_url":"https://api.github.com/users/trustin/events{/privacy}","received_events_url":"https://api.github.com/users/trustin/received_events","type":"User","site_admin":false},"open_issues":1,"closed_issues":0,"state":"open","created_at":"2018-05-03T00:52:05Z","updated_at":"2018-05-03T00:52:05Z","due_on":null,"closed_at":null},"commits_url":"https://api.github.com/repos/line/centraldogma/pulls/207/commits","review_comments_url":"https://api.github.com/repos/line/centraldogma/pulls/207/comments","review_comment_url":"https://api.github.com/repos/line/centraldogma/pulls/comments{/number}","comments_url":"https://api.github.com/repos/line/centraldogma/issues/207/comments","statuses_url":"https://api.github.com/repos/line/centraldogma/statuses/6ecbac6afdfc47dfe63225b893cf54e6a4c9507e","head":{"label":"minwoox:add_go_watcher","ref":"add_go_watcher","sha":"6ecbac6afdfc47dfe63225b893cf54e6a4c9507e","user":{"login":"minwoox","id":25103250,"avatar_url":"https://avatars2.githubusercontent.com/u/25103250?v=4","gravatar_id":"","url":"https://api.github.com/users/minwoox","html_url":"https://github.com/minwoox","followers_url":"https://api.github.com/users/minwoox/followers","following_url":"https://api.github.com/users/minwoox/following{/other_user}","gists_url":"https://api.github.com/users/minwoox/gists{/gist_id}","starred_url":"https://api.github.com/users/minwoox/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/minwoox/subscriptions","organizations_url":"https://api.github.com/users/minwoox/orgs","repos_url":"https://api.github.com/users/minwoox/repos","events_url":"https://api.github.com/users/minwoox/events{/privacy}","received_events_url":"https://api.github.com/users/minwoox/received_events","type":"User","site_admin":false},"repo":{"id":105045412,"name":"centraldogma","full_name":"minwoox/centraldogma","owner":{"login":"minwoox","id":25103250,"avatar_url":"https://avatars2.githubusercontent.com/u/25103250?v=4","gravatar_id":"","url":"https://api.github.com/users/minwoox","html_url":"https://github.com/minwoox","followers_url":"https://api.github.com/users/minwoox/followers","following_url":"https://api.github.com/users/minwoox/following{/other_user}","gists_url":"https://api.github.com/users/minwoox/gists{/gist_id}","starred_url":"https://api.github.com/users/minwoox/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/minwoox/subscriptions","organizations_url":"https://api.github.com/users/minwoox/orgs","repos_url":"https://api.github.com/users/minwoox/repos","events_url":"https://api.github.com/users/minwoox/events{/privacy}","received_events_url":"https://api.github.com/users/minwoox/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/minwoox/centraldogma","description":"Highly-available version-controlled service configuration repository based on Git, ZooKeeper and HTTP/2","fork":true,"url":"https://api.github.com/repos/minwoox/centraldogma","forks_url":"https://api.github.com/repos/minwoox/centraldogma/forks","keys_url":"https://api.github.com/repos/minwoox/centraldogma/keys{/key_id}","collaborators_url":"https://api.github.com/repos/minwoox/centraldogma/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/minwoox/centraldogma/teams","hooks_url":"https://api.github.com/repos/minwoox/centraldogma/hooks","issue_events_url":"https://api.github.com/repos/minwoox/centraldogma/issues/events{/number}","events_url":"https://api.github.com/repos/minwoox/centraldogma/events","assignees_url":"https://api.github.com/repos/minwoox/centraldogma/assignees{/user}","branches_url":"https://api.github.com/repos/minwoox/centraldogma/branches{/branch}","tags_url":"https://api.github.com/repos/minwoox/centraldogma/tags","blobs_url":"https://api.github.com/repos/minwoox/centraldogma/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/minwoox/centraldogma/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/minwoox/centraldogma/git/refs{/sha}","trees_url":"https://api.github.com/repos/minwoox/centraldogma/git/trees{/sha}","statuses_url":"https://api.github.com/repos/minwoox/centraldogma/statuses/{sha}","languages_url":"https://api.github.com/repos/minwoox/centraldogma/languages","stargazers_url":"https://api.github.com/repos/minwoox/centraldogma/stargazers","contributors_url":"https://api.github.com/repos/minwoox/centraldogma/contributors","subscribers_url":"https://api.github.com/repos/minwoox/centraldogma/subscribers","subscription_url":"https://api.github.com/repos/minwoox/centraldogma/subscription","commits_url":"https://api.github.com/repos/minwoox/centraldogma/commits{/sha}","git_commits_url":"https://api.github.com/repos/minwoox/centraldogma/git/commits{/sha}","comments_url":"https://api.github.com/repos/minwoox/centraldogma/comments{/number}","issue_comment_url":"https://api.github.com/repos/minwoox/centraldogma/issues/comments{/number}","contents_url":"https://api.github.com/repos/minwoox/centraldogma/contents/{+path}","compare_url":"https://api.github.com/repos/minwoox/centraldogma/compare/{base}...{head}","merges_url":"https://api.github.com/repos/minwoox/centraldogma/merges","archive_url":"https://api.github.com/repos/minwoox/centraldogma/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/minwoox/centraldogma/downloads","issues_url":"https://api.github.com/repos/minwoox/centraldogma/issues{/number}","pulls_url":"https://api.github.com/repos/minwoox/centraldogma/pulls{/number}","milestones_url":"https://api.github.com/repos/minwoox/centraldogma/milestones{/number}","notifications_url":"https://api.github.com/repos/minwoox/centraldogma/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/minwoox/centraldogma/labels{/name}","releases_url":"https://api.github.com/repos/minwoox/centraldogma/releases{/id}","deployments_url":"https://api.github.com/repos/minwoox/centraldogma/deployments","created_at":"2017-09-27T17:00:08Z","updated_at":"2017-09-27T17:00:10Z","pushed_at":"2018-05-02T12:48:58Z","git_url":"git://github.com/minwoox/centraldogma.git","ssh_url":"git@github.com:minwoox/centraldogma.git","clone_url":"https://github.com/minwoox/centraldogma.git","svn_url":"https://github.com/minwoox/centraldogma","homepage":"https://line.github.io/centraldogma/","size":19356,"stargazers_count":0,"watchers_count":0,"language":"Java","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"open_issues_count":0,"license":{"key":"apache-2.0","name":"Apache License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0"},"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"line:master","ref":"master","sha":"71c8b1abab56be78387007eae62ffa853aaf9d92","user":{"login":"line","id":13128444,"avatar_url":"https://avatars2.githubusercontent.com/u/13128444?v=4","gravatar_id":"","url":"https://api.github.com/users/line","html_url":"https://github.com/line","followers_url":"https://api.github.com/users/line/followers","following_url":"https://api.github.com/users/line/following{/other_user}","gists_url":"https://api.github.com/users/line/gists{/gist_id}","starred_url":"https://api.github.com/users/line/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/line/subscriptions","organizations_url":"https://api.github.com/users/line/orgs","repos_url":"https://api.github.com/users/line/repos","events_url":"https://api.github.com/users/line/events{/privacy}","received_events_url":"https://api.github.com/users/line/received_events","type":"Organization","site_admin":false},"repo":{"id":101732174,"name":"centraldogma","full_name":"line/centraldogma","owner":{"login":"line","id":13128444,"avatar_url":"https://avatars2.githubusercontent.com/u/13128444?v=4","gravatar_id":"","url":"https://api.github.com/users/line","html_url":"https://github.com/line","followers_url":"https://api.github.com/users/line/followers","following_url":"https://api.github.com/users/line/following{/other_user}","gists_url":"https://api.github.com/users/line/gists{/gist_id}","starred_url":"https://api.github.com/users/line/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/line/subscriptions","organizations_url":"https://api.github.com/users/line/orgs","repos_url":"https://api.github.com/users/line/repos","events_url":"https://api.github.com/users/line/events{/privacy}","received_events_url":"https://api.github.com/users/line/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/line/centraldogma","description":"Highly-available version-controlled service configuration repository based on Git, ZooKeeper and HTTP/2","fork":false,"url":"https://api.github.com/repos/line/centraldogma","forks_url":"https://api.github.com/repos/line/centraldogma/forks","keys_url":"https://api.github.com/repos/line/centraldogma/keys{/key_id}","collaborators_url":"https://api.github.com/repos/line/centraldogma/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/line/centraldogma/teams","hooks_url":"https://api.github.com/repos/line/centraldogma/hooks","issue_events_url":"https://api.github.com/repos/line/centraldogma/issues/events{/number}","events_url":"https://api.github.com/repos/line/centraldogma/events","assignees_url":"https://api.github.com/repos/line/centraldogma/assignees{/user}","branches_url":"https://api.github.com/repos/line/centraldogma/branches{/branch}","tags_url":"https://api.github.com/repos/line/centraldogma/tags","blobs_url":"https://api.github.com/repos/line/centraldogma/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/line/centraldogma/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/line/centraldogma/git/refs{/sha}","trees_url":"https://api.github.com/repos/line/centraldogma/git/trees{/sha}","statuses_url":"https://api.github.com/repos/line/centraldogma/statuses/{sha}","languages_url":"https://api.github.com/repos/line/centraldogma/languages","stargazers_url":"https://api.github.com/repos/line/centraldogma/stargazers","contributors_url":"https://api.github.com/repos/line/centraldogma/contributors","subscribers_url":"https://api.github.com/repos/line/centraldogma/subscribers","subscription_url":"https://api.github.com/repos/line/centraldogma/subscription","commits_url":"https://api.github.com/repos/line/centraldogma/commits{/sha}","git_commits_url":"https://api.github.com/repos/line/centraldogma/git/commits{/sha}","comments_url":"https://api.github.com/repos/line/centraldogma/comments{/number}","issue_comment_url":"https://api.github.com/repos/line/centraldogma/issues/comments{/number}","contents_url":"https://api.github.com/repos/line/centraldogma/contents/{+path}","compare_url":"https://api.github.com/repos/line/centraldogma/compare/{base}...{head}","merges_url":"https://api.github.com/repos/line/centraldogma/merges","archive_url":"https://api.github.com/repos/line/centraldogma/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/line/centraldogma/downloads","issues_url":"https://api.github.com/repos/line/centraldogma/issues{/number}","pulls_url":"https://api.github.com/repos/line/centraldogma/pulls{/number}","milestones_url":"https://api.github.com/repos/line/centraldogma/milestones{/number}","notifications_url":"https://api.github.com/repos/line/centraldogma/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/line/centraldogma/labels{/name}","releases_url":"https://api.github.com/repos/line/centraldogma/releases{/id}","deployments_url":"https://api.github.com/repos/line/centraldogma/deployments","created_at":"2017-08-29T07:23:31Z","updated_at":"2018-05-02T00:33:30Z","pushed_at":"2018-05-02T12:49:00Z","git_url":"git://github.com/line/centraldogma.git","ssh_url":"git@github.com:line/centraldogma.git","clone_url":"https://github.com/line/centraldogma.git","svn_url":"https://github.com/line/centraldogma","homepage":"https://line.github.io/centraldogma/","size":22312,"stargazers_count":157,"watchers_count":157,"language":"Java","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":true,"forks_count":39,"mirror_url":null,"archived":false,"open_issues_count":58,"license":{"key":"apache-2.0","name":"Apache License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0"},"forks":39,"open_issues":58,"watchers":157,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/line/centraldogma/pulls/207"},"html":{"href":"https://github.com/line/centraldogma/pull/207"},"issue":{"href":"https://api.github.com/repos/line/centraldogma/issues/207"},"comments":{"href":"https://api.github.com/repos/line/centraldogma/issues/207/comments"},"review_comments":{"href":"https://api.github.com/repos/line/centraldogma/pulls/207/comments"},"review_comment":{"href":"https://api.github.com/repos/line/centraldogma/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/line/centraldogma/pulls/207/commits"},"statuses":{"href":"https://api.github.com/repos/line/centraldogma/statuses/6ecbac6afdfc47dfe63225b893cf54e6a4c9507e"}},"author_association":"MEMBER"}}
{"actor":{"display_login":"minwoox"}}