{"action":"created","comment":{"url":"https://api.github.com/repos/containers/image/pulls/comments/91839275","pull_request_review_id":12351791,"id":91839275,"diff_hunk":"@@ -0,0 +1,598 @@\n+package storage\n+\n+import (\n+\t\"bytes\"\n+\t\"encoding/json\"\n+\t\"errors\"\n+\t\"fmt\"\n+\t\"io\"\n+\t\"io/ioutil\"\n+\t\"time\"\n+\n+\t\"github.com/Sirupsen/logrus\"\n+\t\"github.com/containers/image/image\"\n+\t\"github.com/containers/image/manifest\"\n+\t\"github.com/containers/image/types\"\n+\t\"github.com/containers/storage/pkg/archive\"\n+\t\"github.com/containers/storage/pkg/ioutils\"\n+\t\"github.com/containers/storage/storage\"\n+\tddigest \"github.com/docker/distribution/digest\"\n+)\n+\n+var (\n+\t// ErrBlobDigestMismatch is returned when PutBlob() is given a blob\n+\t// with a digest-based name that doesn't match its contents.\n+\tErrBlobDigestMismatch = errors.New(\"blob digest mismatch\")\n+\t// ErrBlobSizeMismatch is returned when PutBlob() is given a blob\n+\t// with an expected size that doesn't match the reader.\n+\tErrBlobSizeMismatch = errors.New(\"blob size mismatch\")\n+\t// ErrNoManifestLists is returned when GetTargetManifest() is\n+\t// called.\n+\tErrNoManifestLists = errors.New(\"manifest lists are not supported by this transport\")\n+\t// ErrNoSuchImage is returned when we attempt to access an image which\n+\t// doesn't exist in the storage area.\n+\tErrNoSuchImage = storage.ErrNotAnImage\n+)\n+\n+type storageImageSource struct {\n+\timageRef       storageReference\n+\tTag            string                      `json:\"tag,omitempty\"`\n+\tCreated        time.Time                   `json:\"created-time,omitempty\"`\n+\tID             string                      `json:\"id\"`\n+\tBlobList       []types.BlobInfo            `json:\"blob-list,omitempty\"` // Ordered list of every blob the image has been told to handle\n+\tLayers         map[ddigest.Digest][]string `json:\"layers,omitempty\"`    // Map from digests of blobs to lists of layer IDs\n+\tLayerPosition  map[ddigest.Digest]int      `json:\"-\"`                   // Where we are in reading a blob's layers\n+\tSignatureSizes []int                       `json:\"signature-sizes\"`     // List of sizes of each signature slice\n+}\n+\n+type storageImageDestination struct {\n+\timageRef       storageReference\n+\tTag            string                      `json:\"tag,omitempty\"`\n+\tCreated        time.Time                   `json:\"created-time,omitempty\"`\n+\tID             string                      `json:\"id\"`\n+\tBlobList       []types.BlobInfo            `json:\"blob-list,omitempty\"` // Ordered list of every blob the image has been told to handle\n+\tLayers         map[ddigest.Digest][]string `json:\"layers,omitempty\"`    // Map from digests of blobs to lists of layer IDs\n+\tBlobData       map[ddigest.Digest][]byte   `json:\"-\"`                   // Map from names of blobs that aren't layers to contents, temporary\n+\tManifest       []byte                      `json:\"-\"`                   // Manifest contents, temporary\n+\tSignatures     []byte                      `json:\"-\"`                   // Signature contents, temporary\n+\tSignatureSizes []int                       `json:\"signature-sizes\"`     // List of sizes of each signature slice\n+}\n+\n+type storageLayerMetadata struct {\n+\tDigest         string `json:\"digest,omitempty\"`\n+\tSize           int64  `json:\"size\"`\n+\tCompressedSize int64  `json:\"compressed-size,omitempty\"`\n+}\n+\n+type storageImage struct {\n+\ttypes.Image\n+\tsize int64\n+}\n+\n+// A Sizer is an object which can return the size of its contents, or -1 if it\n+// knows how but couldn't find the answer.\n+type Sizer interface {\n+\tSize() (int64, error)\n+}\n+\n+// newImageSource sets us up to read out an image, which needs to already exist.\n+func newImageSource(imageRef storageReference) (*storageImageSource, error) {\n+\tid := imageRef.resolveID()\n+\tif id == \"\" {\n+\t\tlogrus.Errorf(\"no image matching reference %q found\", imageRef.StringWithinTransport())\n+\t\treturn nil, ErrNoSuchImage\n+\t}\n+\timg, err := imageRef.transport.store.GetImage(id)\n+\tif err != nil {\n+\t\tlogrus.Errorf(\"error reading image %q: %v\", id, err)\n+\t\treturn nil, err\n+\t}\n+\timage := storageImageSource{\n+\t\timageRef:       imageRef,\n+\t\tCreated:        time.Now(),\n+\t\tID:             img.ID,\n+\t\tBlobList:       []types.BlobInfo{},\n+\t\tLayers:         make(map[ddigest.Digest][]string),\n+\t\tLayerPosition:  make(map[ddigest.Digest]int),\n+\t\tSignatureSizes: []int{},\n+\t}\n+\tif err := json.Unmarshal([]byte(img.Metadata), &image); err != nil {\n+\t\tlogrus.Errorf(\"error decoding metadata for source image: %v\", err)\n+\t\treturn nil, err\n+\t}\n+\treturn &image, nil\n+}\n+\n+// newImageDestination sets us up to write a new image.\n+func newImageDestination(imageRef storageReference) (*storageImageDestination, error) {\n+\timage := storageImageDestination{\n+\t\timageRef:       imageRef,\n+\t\tTag:            imageRef.reference,\n+\t\tCreated:        time.Now(),\n+\t\tID:             imageRef.id,\n+\t\tBlobList:       []types.BlobInfo{},\n+\t\tLayers:         make(map[ddigest.Digest][]string),\n+\t\tBlobData:       make(map[ddigest.Digest][]byte),\n+\t\tSignatureSizes: []int{},\n+\t}\n+\treturn &image, nil\n+}\n+\n+func (s storageImageSource) Reference() types.ImageReference {\n+\treturn s.imageRef\n+}\n+\n+func (s storageImageDestination) Reference() types.ImageReference {\n+\treturn s.imageRef\n+}\n+\n+func (s storageImageSource) Close() {\n+}\n+\n+func (s storageImageDestination) Close() {\n+}\n+\n+func (s storageImageDestination) ShouldCompressLayers() bool {\n+\t// We ultimately have to decompress layers to populate trees on disk,\n+\t// so callers shouldn't bother compressing them before handing them to\n+\t// us, if they're not already compressed.\n+\treturn false\n+}\n+\n+// PutBlob is used to both store filesystem layers and binary data that is part\n+// of the image.  Filesystem layers are assumed to be imported in order, as\n+// that is required by some of the underlying storage drivers.\n+func (s *storageImageDestination) PutBlob(stream io.Reader, blobinfo types.BlobInfo) (types.BlobInfo, error) {\n+\tblobSize := int64(-1)\n+\tdigest := blobinfo.Digest\n+\terrorBlobInfo := types.BlobInfo{\n+\t\tDigest: \"\",\n+\t\tSize:   -1,\n+\t}\n+\t// Try to read an initial snippet of the blob.\n+\theader := make([]byte, 10240)\n+\tn, err := stream.Read(header)\n+\tif err != nil && err != io.EOF {\n+\t\treturn errorBlobInfo, err\n+\t}\n+\t// Set up to read the whole blob (the initial snippet, plus the rest)\n+\t// while digesting it with either the default, or the passed-in digest,\n+\t// if one was specified.\n+\thasher := ddigest.Canonical.New()\n+\tif digest.Validate() == nil {\n+\t\tif a := digest.Algorithm(); a.Available() {\n+\t\t\thasher = a.New()\n+\t\t}\n+\t}\n+\thash := \"\"\n+\tcounter := ioutils.NewWriteCounter(hasher.Hash())\n+\tdefragmented := io.MultiReader(bytes.NewBuffer(header[:n]), stream)\n+\tmulti := io.TeeReader(defragmented, counter)\n+\tif (n > 0) && archive.IsArchive(header[:n]) {\n+\t\t// It's a filesystem layer.  If it's not the first one in the\n+\t\t// image, we assume that the most recently added layer is its\n+\t\t// parent.\n+\t\tparentLayer := \"\"\n+\t\tif len(s.BlobList) > 0 {\n+\t\t\tfor _, blob := range s.BlobList {\n+\t\t\t\tif layerList, ok := s.Layers[blob.Digest]; ok {\n+\t\t\t\t\tparentLayer = layerList[len(layerList)-1]\n+\t\t\t\t}\n+\t\t\t}\n+\t\t}\n+\t\t// If we have an expected content digest, generate a layer ID\n+\t\t// based on the parent's ID and the expected content digest.\n+\t\tid := \"\"\n+\t\tif digest.Validate() == nil {\n+\t\t\tid = ddigest.Canonical.FromBytes([]byte(parentLayer + \"+\" + digest.String())).Hex()\n+\t\t}\n+\t\t// Attempt to create the identified layer and import its contents.\n+\t\tlayer, uncompressedSize, err := s.imageRef.transport.store.PutLayer(id, parentLayer, nil, \"\", true, multi)\n+\t\tif err != nil && err != storage.ErrDuplicateID {\n+\t\t\tlogrus.Debugf(\"error importing layer blob %q as %q: %v\", blobinfo.Digest, id, err)\n+\t\t\treturn errorBlobInfo, err\n+\t\t}\n+\t\tif err == storage.ErrDuplicateID {\n+\t\t\t// We specified an ID, and there's already a layer with\n+\t\t\t// the same ID.  Drain the input so that we can look at\n+\t\t\t// its length and digest.\n+\t\t\t_, err := io.Copy(ioutil.Discard, multi)\n+\t\t\tif err != nil && err != io.EOF {\n+\t\t\t\tlogrus.Debugf(\"error digesting layer blob %q: %v\", blobinfo.Digest, id, err)\n+\t\t\t\treturn errorBlobInfo, err\n+\t\t\t}\n+\t\t\thash = hasher.Digest().String()\n+\t\t} else {\n+\t\t\t// Applied the layer with the specified ID.  Note the\n+\t\t\t// size info and computed digest.\n+\t\t\thash = hasher.Digest().String()\n+\t\t\tlayerMeta := storageLayerMetadata{\n+\t\t\t\tDigest:         hash,\n+\t\t\t\tCompressedSize: counter.Count,\n+\t\t\t\tSize:           uncompressedSize,\n+\t\t\t}\n+\t\t\tif metadata, err := json.Marshal(layerMeta); len(metadata) != 0 && err == nil {\n+\t\t\t\ts.imageRef.transport.store.SetMetadata(layer.ID, string(metadata))\n+\t\t\t}\n+\t\t\t// Hang on to the new layer's ID.\n+\t\t\tid = layer.ID\n+\t\t}\n+\t\tblobSize = counter.Count\n+\t\t// Check if the size looks right.\n+\t\tif blobinfo.Size >= 0 && blobSize != blobinfo.Size {\n+\t\t\tlogrus.Debugf(\"blob %q size is %d, not %d, rejecting\", blobinfo.Digest, blobSize, blobinfo.Size)\n+\t\t\tif layer != nil {\n+\t\t\t\t// Something's wrong; delete the newly-created layer.\n+\t\t\t\ts.imageRef.transport.store.DeleteLayer(layer.ID)\n+\t\t\t}\n+\t\t\treturn errorBlobInfo, ErrBlobSizeMismatch\n+\t\t}\n+\t\t// If the content digest was specified, verify it.\n+\t\tif digest.Validate() == nil && digest.String() != hash {\n+\t\t\tlogrus.Debugf(\"blob %q digests to %q, rejecting\", blobinfo.Digest, hash)\n+\t\t\tif layer != nil {\n+\t\t\t\t// Something's wrong; delete the newly-created layer.\n+\t\t\t\ts.imageRef.transport.store.DeleteLayer(layer.ID)\n+\t\t\t}\n+\t\t\treturn errorBlobInfo, ErrBlobDigestMismatch\n+\t\t}\n+\t\t// If we didn't get a digest, construct one.\n+\t\tif digest == \"\" {\n+\t\t\tdigest = ddigest.Digest(hash)\n+\t\t}\n+\t\t// Record that this layer blob is a layer, and the layer ID it\n+\t\t// ended up having.  This is a list, in case the same blob is\n+\t\t// being applied more than once.\n+\t\ts.Layers[digest] = append(s.Layers[digest], id)\n+\t\ts.BlobList = append(s.BlobList, types.BlobInfo{Digest: digest, Size: blobSize})\n+\t\tif layer != nil {\n+\t\t\tlogrus.Debugf(\"blob %q imported as a filesystem layer %q\", blobinfo.Digest, id)\n+\t\t} else {\n+\t\t\tlogrus.Debugf(\"layer blob %q already present as layer %q\", blobinfo.Digest, id)\n+\t\t}\n+\t} else {\n+\t\t// It's just data.  Finish scanning it in, check that our\n+\t\t// computed digest matches the passed-in digest, and store it,\n+\t\t// but leave it out of the blob-to-layer-ID map so that we can\n+\t\t// tell that it's not a layer.\n+\t\tblob, err := ioutil.ReadAll(multi)\n+\t\tif err != nil && err != io.EOF {\n+\t\t\treturn errorBlobInfo, err\n+\t\t}\n+\t\tblobSize = int64(len(blob))\n+\t\thash = hasher.Digest().String()\n+\t\tif blobinfo.Size >= 0 && blobSize != blobinfo.Size {\n+\t\t\tlogrus.Debugf(\"blob %q size is %d, not %d, rejecting\", blobinfo.Digest, blobSize, blobinfo.Size)\n+\t\t\treturn errorBlobInfo, ErrBlobSizeMismatch\n+\t\t}\n+\t\t// If we were given a digest, verify that the content matches\n+\t\t// it.\n+\t\tif digest.Validate() == nil && digest.String() != hash {\n+\t\t\tlogrus.Debugf(\"blob %q digests to %q, rejecting\", blobinfo.Digest, hash)\n+\t\t\treturn errorBlobInfo, ErrBlobDigestMismatch\n+\t\t}\n+\t\t// If we didn't get a digest, construct one.\n+\t\tif digest == \"\" {\n+\t\t\tdigest = ddigest.Digest(hash)\n+\t\t}\n+\t\t// Save the blob for when we Commit().\n+\t\ts.BlobData[digest] = blob\n+\t\ts.BlobList = append(s.BlobList, types.BlobInfo{Digest: digest, Size: blobSize})\n+\t\tlogrus.Debugf(\"blob %q imported as opaque data %q\", blobinfo.Digest, digest)\n+\t}\n+\treturn types.BlobInfo{\n+\t\tDigest: digest,\n+\t\tSize:   blobSize,\n+\t}, nil\n+}\n+\n+func (s *storageImageDestination) HasBlob(blobinfo types.BlobInfo) (bool, int64, error) {\n+\tif blobinfo.Digest == \"\" {\n+\t\treturn false, -1, fmt.Errorf(`\"Can not check for a blob with unknown digest`)\n+\t}\n+\tfor _, blob := range s.BlobList {","path":"storage/storage_image.go","position":293,"original_position":293,"commit_id":"7d14a9f0b452fa7779c740b619dc7468beca143c","original_commit_id":"7d14a9f0b452fa7779c740b619dc7468beca143c","user":{"login":"runcom","id":849915,"avatar_url":"https://avatars.githubusercontent.com/u/849915?v=3","gravatar_id":"","url":"https://api.github.com/users/runcom","html_url":"https://github.com/runcom","followers_url":"https://api.github.com/users/runcom/followers","following_url":"https://api.github.com/users/runcom/following{/other_user}","gists_url":"https://api.github.com/users/runcom/gists{/gist_id}","starred_url":"https://api.github.com/users/runcom/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/runcom/subscriptions","organizations_url":"https://api.github.com/users/runcom/orgs","repos_url":"https://api.github.com/users/runcom/repos","events_url":"https://api.github.com/users/runcom/events{/privacy}","received_events_url":"https://api.github.com/users/runcom/received_events","type":"User","site_admin":false},"body":"can `s.BlobList` be a map indexed by digest and make this faster?","created_at":"2016-12-10T17:53:36Z","updated_at":"2016-12-10T17:53:36Z","html_url":"https://github.com/containers/image/pull/63#discussion_r91839275","pull_request_url":"https://api.github.com/repos/containers/image/pulls/63","_links":{"self":{"href":"https://api.github.com/repos/containers/image/pulls/comments/91839275"},"html":{"href":"https://github.com/containers/image/pull/63#discussion_r91839275"},"pull_request":{"href":"https://api.github.com/repos/containers/image/pulls/63"}}},"pull_request":{"url":"https://api.github.com/repos/containers/image/pulls/63","id":83463408,"html_url":"https://github.com/containers/image/pull/63","diff_url":"https://github.com/containers/image/pull/63.diff","patch_url":"https://github.com/containers/image/pull/63.patch","issue_url":"https://api.github.com/repos/containers/image/issues/63","number":63,"state":"open","locked":false,"title":"Add support for containers/storage","user":{"login":"nalind","id":1200159,"avatar_url":"https://avatars.githubusercontent.com/u/1200159?v=3","gravatar_id":"","url":"https://api.github.com/users/nalind","html_url":"https://github.com/nalind","followers_url":"https://api.github.com/users/nalind/followers","following_url":"https://api.github.com/users/nalind/following{/other_user}","gists_url":"https://api.github.com/users/nalind/gists{/gist_id}","starred_url":"https://api.github.com/users/nalind/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/nalind/subscriptions","organizations_url":"https://api.github.com/users/nalind/orgs","repos_url":"https://api.github.com/users/nalind/repos","events_url":"https://api.github.com/users/nalind/events{/privacy}","received_events_url":"https://api.github.com/users/nalind/received_events","type":"User","site_admin":false},"body":"This patch adds containers/storage as a backend type called 'oci-storage', using a storage.Mall initialized with the library's default settings, or using an externally-initialized storage.Mall.\n\nAn image's blobs are stored either as oci-storage layers (if they look like filesystem layers) or (alongside the manifest and signatures) as named big data items associated with the image.\n\nRecord-keeping (so that we can remember which blobs were layers and which weren't) are encoded as a JSON object which is stored as the oci-storage image's metadata.\n\nThe type of compression which we detected around a layer blob when it was imported is encoded as a field in a JSON object which is stored as the oci-storage layer object's metadata, and is reapplied if we need to export the layer later.\n\nIf we find ourselves importing an image with the same ID as an image that we already have, we return an error.  If we find ourselves importing an image which contains layers which we already have, we flag\nan error if its contents differ from the one we already have, otherwise we accept it.\n\nIf we find ourselves importing an image that wants to be tagged with a name that is already in use, the name is assigned to the new image and the old image remains otherwise unmodified.\n","created_at":"2016-08-31T14:27:56Z","updated_at":"2016-12-10T17:53:36Z","closed_at":null,"merged_at":null,"merge_commit_sha":"b84ed087b9d91d3705d6aad031e675f0d92b41f9","assignee":null,"assignees":[],"milestone":null,"commits_url":"https://api.github.com/repos/containers/image/pulls/63/commits","review_comments_url":"https://api.github.com/repos/containers/image/pulls/63/comments","review_comment_url":"https://api.github.com/repos/containers/image/pulls/comments{/number}","comments_url":"https://api.github.com/repos/containers/image/issues/63/comments","statuses_url":"https://api.github.com/repos/containers/image/statuses/7d14a9f0b452fa7779c740b619dc7468beca143c","head":{"label":"nalind:add-storage-transport","ref":"add-storage-transport","sha":"7d14a9f0b452fa7779c740b619dc7468beca143c","user":{"login":"nalind","id":1200159,"avatar_url":"https://avatars.githubusercontent.com/u/1200159?v=3","gravatar_id":"","url":"https://api.github.com/users/nalind","html_url":"https://github.com/nalind","followers_url":"https://api.github.com/users/nalind/followers","following_url":"https://api.github.com/users/nalind/following{/other_user}","gists_url":"https://api.github.com/users/nalind/gists{/gist_id}","starred_url":"https://api.github.com/users/nalind/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/nalind/subscriptions","organizations_url":"https://api.github.com/users/nalind/orgs","repos_url":"https://api.github.com/users/nalind/repos","events_url":"https://api.github.com/users/nalind/events{/privacy}","received_events_url":"https://api.github.com/users/nalind/received_events","type":"User","site_admin":false},"repo":{"id":65415381,"name":"image","full_name":"nalind/image","owner":{"login":"nalind","id":1200159,"avatar_url":"https://avatars.githubusercontent.com/u/1200159?v=3","gravatar_id":"","url":"https://api.github.com/users/nalind","html_url":"https://github.com/nalind","followers_url":"https://api.github.com/users/nalind/followers","following_url":"https://api.github.com/users/nalind/following{/other_user}","gists_url":"https://api.github.com/users/nalind/gists{/gist_id}","starred_url":"https://api.github.com/users/nalind/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/nalind/subscriptions","organizations_url":"https://api.github.com/users/nalind/orgs","repos_url":"https://api.github.com/users/nalind/repos","events_url":"https://api.github.com/users/nalind/events{/privacy}","received_events_url":"https://api.github.com/users/nalind/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/nalind/image","description":"Work with containers' images","fork":true,"url":"https://api.github.com/repos/nalind/image","forks_url":"https://api.github.com/repos/nalind/image/forks","keys_url":"https://api.github.com/repos/nalind/image/keys{/key_id}","collaborators_url":"https://api.github.com/repos/nalind/image/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/nalind/image/teams","hooks_url":"https://api.github.com/repos/nalind/image/hooks","issue_events_url":"https://api.github.com/repos/nalind/image/issues/events{/number}","events_url":"https://api.github.com/repos/nalind/image/events","assignees_url":"https://api.github.com/repos/nalind/image/assignees{/user}","branches_url":"https://api.github.com/repos/nalind/image/branches{/branch}","tags_url":"https://api.github.com/repos/nalind/image/tags","blobs_url":"https://api.github.com/repos/nalind/image/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/nalind/image/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/nalind/image/git/refs{/sha}","trees_url":"https://api.github.com/repos/nalind/image/git/trees{/sha}","statuses_url":"https://api.github.com/repos/nalind/image/statuses/{sha}","languages_url":"https://api.github.com/repos/nalind/image/languages","stargazers_url":"https://api.github.com/repos/nalind/image/stargazers","contributors_url":"https://api.github.com/repos/nalind/image/contributors","subscribers_url":"https://api.github.com/repos/nalind/image/subscribers","subscription_url":"https://api.github.com/repos/nalind/image/subscription","commits_url":"https://api.github.com/repos/nalind/image/commits{/sha}","git_commits_url":"https://api.github.com/repos/nalind/image/git/commits{/sha}","comments_url":"https://api.github.com/repos/nalind/image/comments{/number}","issue_comment_url":"https://api.github.com/repos/nalind/image/issues/comments{/number}","contents_url":"https://api.github.com/repos/nalind/image/contents/{+path}","compare_url":"https://api.github.com/repos/nalind/image/compare/{base}...{head}","merges_url":"https://api.github.com/repos/nalind/image/merges","archive_url":"https://api.github.com/repos/nalind/image/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/nalind/image/downloads","issues_url":"https://api.github.com/repos/nalind/image/issues{/number}","pulls_url":"https://api.github.com/repos/nalind/image/pulls{/number}","milestones_url":"https://api.github.com/repos/nalind/image/milestones{/number}","notifications_url":"https://api.github.com/repos/nalind/image/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/nalind/image/labels{/name}","releases_url":"https://api.github.com/repos/nalind/image/releases{/id}","deployments_url":"https://api.github.com/repos/nalind/image/deployments","created_at":"2016-08-10T20:46:37Z","updated_at":"2016-08-10T20:46:38Z","pushed_at":"2016-12-08T17:48:03Z","git_url":"git://github.com/nalind/image.git","ssh_url":"git@github.com:nalind/image.git","clone_url":"https://github.com/nalind/image.git","svn_url":"https://github.com/nalind/image","homepage":"","size":2786,"stargazers_count":0,"watchers_count":0,"language":"Go","has_issues":false,"has_downloads":true,"has_wiki":true,"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":"containers:master","ref":"master","sha":"bad049ae43b296d84e83b204d27f7df8751b460d","user":{"login":"containers","id":5874934,"avatar_url":"https://avatars.githubusercontent.com/u/5874934?v=3","gravatar_id":"","url":"https://api.github.com/users/containers","html_url":"https://github.com/containers","followers_url":"https://api.github.com/users/containers/followers","following_url":"https://api.github.com/users/containers/following{/other_user}","gists_url":"https://api.github.com/users/containers/gists{/gist_id}","starred_url":"https://api.github.com/users/containers/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/containers/subscriptions","organizations_url":"https://api.github.com/users/containers/orgs","repos_url":"https://api.github.com/users/containers/repos","events_url":"https://api.github.com/users/containers/events{/privacy}","received_events_url":"https://api.github.com/users/containers/received_events","type":"Organization","site_admin":false},"repo":{"id":61739430,"name":"image","full_name":"containers/image","owner":{"login":"containers","id":5874934,"avatar_url":"https://avatars.githubusercontent.com/u/5874934?v=3","gravatar_id":"","url":"https://api.github.com/users/containers","html_url":"https://github.com/containers","followers_url":"https://api.github.com/users/containers/followers","following_url":"https://api.github.com/users/containers/following{/other_user}","gists_url":"https://api.github.com/users/containers/gists{/gist_id}","starred_url":"https://api.github.com/users/containers/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/containers/subscriptions","organizations_url":"https://api.github.com/users/containers/orgs","repos_url":"https://api.github.com/users/containers/repos","events_url":"https://api.github.com/users/containers/events{/privacy}","received_events_url":"https://api.github.com/users/containers/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/containers/image","description":"Work with containers' images","fork":false,"url":"https://api.github.com/repos/containers/image","forks_url":"https://api.github.com/repos/containers/image/forks","keys_url":"https://api.github.com/repos/containers/image/keys{/key_id}","collaborators_url":"https://api.github.com/repos/containers/image/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/containers/image/teams","hooks_url":"https://api.github.com/repos/containers/image/hooks","issue_events_url":"https://api.github.com/repos/containers/image/issues/events{/number}","events_url":"https://api.github.com/repos/containers/image/events","assignees_url":"https://api.github.com/repos/containers/image/assignees{/user}","branches_url":"https://api.github.com/repos/containers/image/branches{/branch}","tags_url":"https://api.github.com/repos/containers/image/tags","blobs_url":"https://api.github.com/repos/containers/image/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/containers/image/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/containers/image/git/refs{/sha}","trees_url":"https://api.github.com/repos/containers/image/git/trees{/sha}","statuses_url":"https://api.github.com/repos/containers/image/statuses/{sha}","languages_url":"https://api.github.com/repos/containers/image/languages","stargazers_url":"https://api.github.com/repos/containers/image/stargazers","contributors_url":"https://api.github.com/repos/containers/image/contributors","subscribers_url":"https://api.github.com/repos/containers/image/subscribers","subscription_url":"https://api.github.com/repos/containers/image/subscription","commits_url":"https://api.github.com/repos/containers/image/commits{/sha}","git_commits_url":"https://api.github.com/repos/containers/image/git/commits{/sha}","comments_url":"https://api.github.com/repos/containers/image/comments{/number}","issue_comment_url":"https://api.github.com/repos/containers/image/issues/comments{/number}","contents_url":"https://api.github.com/repos/containers/image/contents/{+path}","compare_url":"https://api.github.com/repos/containers/image/compare/{base}...{head}","merges_url":"https://api.github.com/repos/containers/image/merges","archive_url":"https://api.github.com/repos/containers/image/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/containers/image/downloads","issues_url":"https://api.github.com/repos/containers/image/issues{/number}","pulls_url":"https://api.github.com/repos/containers/image/pulls{/number}","milestones_url":"https://api.github.com/repos/containers/image/milestones{/number}","notifications_url":"https://api.github.com/repos/containers/image/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/containers/image/labels{/name}","releases_url":"https://api.github.com/repos/containers/image/releases{/id}","deployments_url":"https://api.github.com/repos/containers/image/deployments","created_at":"2016-06-22T17:46:41Z","updated_at":"2016-11-12T00:57:15Z","pushed_at":"2016-12-09T16:35:03Z","git_url":"git://github.com/containers/image.git","ssh_url":"git@github.com:containers/image.git","clone_url":"https://github.com/containers/image.git","svn_url":"https://github.com/containers/image","homepage":"","size":2857,"stargazers_count":59,"watchers_count":59,"language":"Go","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":25,"mirror_url":null,"open_issues_count":36,"forks":25,"open_issues":36,"watchers":59,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/containers/image/pulls/63"},"html":{"href":"https://github.com/containers/image/pull/63"},"issue":{"href":"https://api.github.com/repos/containers/image/issues/63"},"comments":{"href":"https://api.github.com/repos/containers/image/issues/63/comments"},"review_comments":{"href":"https://api.github.com/repos/containers/image/pulls/63/comments"},"review_comment":{"href":"https://api.github.com/repos/containers/image/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/containers/image/pulls/63/commits"},"statuses":{"href":"https://api.github.com/repos/containers/image/statuses/7d14a9f0b452fa7779c740b619dc7468beca143c"}}}}
{"actor":{"display_login":"runcom"}}