{"action":"created","comment":{"url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/pulls/comments/75658707","id":75658707,"diff_hunk":"@@ -0,0 +1,444 @@\n+/*\n+ * The MIT License\n+ *\n+ * Copyright (c) 2016, CloudBees, Inc.\n+ *\n+ * Permission is hereby granted, free of charge, to any person obtaining a copy\n+ * of this software and associated documentation files (the \"Software\"), to deal\n+ * in the Software without restriction, including without limitation the rights\n+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n+ * copies of the Software, and to permit persons to whom the Software is\n+ * furnished to do so, subject to the following conditions:\n+ *\n+ * The above copyright notice and this permission notice shall be included in\n+ * all copies or substantial portions of the Software.\n+ *\n+ * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n+ * THE SOFTWARE.\n+ */\n+\n+package org.jenkinsci.plugins.workflow.graphanalysis;\n+\n+import org.jenkinsci.plugins.workflow.graph.BlockEndNode;\n+import org.jenkinsci.plugins.workflow.graph.BlockStartNode;\n+import org.jenkinsci.plugins.workflow.graph.FlowEndNode;\n+import org.jenkinsci.plugins.workflow.graph.FlowNode;\n+\n+import javax.annotation.CheckForNull;\n+import javax.annotation.Nonnull;\n+import java.util.ArrayDeque;\n+import java.util.ArrayList;\n+import java.util.Collection;\n+import java.util.HashMap;\n+import java.util.LinkedHashSet;\n+import java.util.List;\n+import java.util.ListIterator;\n+import java.util.Set;\n+\n+/**\n+ * Scanner that will scan down all forks when we hit parallel blocks before continuing, but generally runs in linear order\n+ * <p/>Think of it as the opposite of {@link DepthFirstScanner}.\n+ *\n+ * <p/>This is a fairly efficient way to visit all FlowNodes, and provides three useful guarantees:\n+ * <ul>\n+ *   <li>Every FlowNode is visited, and visited EXACTLY ONCE (not true for LinearScanner)</li>\n+ *   <li>All parallel branches are visited before we move past the parallel block (not true for DepthFirstScanner)</li>\n+ *   <li>For EVERY block, the BlockEndNode is visited before the BlockStartNode (not true for DepthFirstScanner, with parallels)</li>\n+ * </ul>\n+ *\n+ * <p/>The big advantages of this approach:\n+ * <ul>\n+ *     <li>Blocks are visited in the order they end (no backtracking) - helps with working a block at a time</li>\n+ *     <li>Points are visited in linear order within a block (easy to use for analysis)</li>\n+ *     <li>Minimal state information needed</li>\n+ *     <li>Branch information is available for use here</li>\n+ * </ul>\n+ *\n+ * @author <samvanoort@gmail.com>Sam Van Oort</samvanoort@gmail.com>\n+ */\n+public class ForkScanner extends AbstractFlowScanner {\n+\n+    // Last element in stack is end of myCurrent parallel start, first is myCurrent start\n+    ArrayDeque<ParallelBlockStart> parallelBlockStartStack = new ArrayDeque<ParallelBlockStart>();\n+\n+    /** FlowNode that will terminate the myCurrent parallel block */\n+    FlowNode currentParallelStartNode = null;\n+\n+    ParallelBlockStart currentParallelStart = null;\n+\n+    private boolean walkingFromFinish = false;\n+\n+    @Override\n+    protected void reset() {\n+        parallelBlockStartStack.clear();\n+        currentParallelStart = null;\n+        currentParallelStartNode = null;\n+        myCurrent = null;\n+        myNext = null;\n+    }\n+\n+    /** If true, we are walking from the flow end node and have a complete view of the flow */\n+    public boolean isWalkingFromFinish() {\n+        return walkingFromFinish;\n+    }\n+\n+    /** Tracks state for parallel blocks, so we can ensure all are visited and know the branch starting point */\n+    protected static class ParallelBlockStart {\n+        protected BlockStartNode forkStart; // This is the node with child branches\n+        protected int remainingBranches;\n+        protected int totalBranches;\n+        protected ArrayDeque<FlowNode> unvisited = new ArrayDeque<FlowNode>();  // Remaining branches of this that we have have not visited yet\n+\n+        protected ParallelBlockStart(BlockStartNode forkStart, int branchCount) {\n+            this.forkStart = forkStart;\n+            this.remainingBranches = branchCount;\n+        }\n+\n+        /** Strictly for internal use in the least common ancestor problem */\n+        ParallelBlockStart() {}\n+    }\n+\n+    interface FlowPiece {  // Mostly a marker\n+        /** If true, this is not a fork and has no following forks */\n+        boolean isLeaf();\n+    }\n+\n+    /** Linear (no parallels) run of FLowNodes */\n+    static class FlowSegment implements FlowPiece {\n+        ArrayList<FlowNode> visited = new ArrayList<FlowNode>();\n+        FlowPiece after;\n+        boolean isLeaf = true;\n+\n+        @Override\n+        public boolean isLeaf() {\n+            return isLeaf;\n+        }\n+\n+        /**\n+         * We have discovered a forking node intersecting our FlowSegment in the middle or meeting at the end\n+         * Now we need to split the flow, or pull out the fork point and make both branches follow it\n+         * @param nodeMapping Mapping of BlockStartNodes to flowpieces (forks or segments)\n+         * @param joinPoint Node where the branches intersect/meet (fork point)\n+         * @param joiningBranch Flow piece that is joining this\n+         * @return Recreated fork\n+         */\n+        Fork split(@Nonnull HashMap<FlowNode, FlowPiece> nodeMapping, @Nonnull BlockStartNode joinPoint, @Nonnull FlowPiece joiningBranch) {\n+            int index = visited.lastIndexOf(joinPoint);  // Fork will be closer to end, so this is better than indexOf\n+            Fork newFork = new Fork(joinPoint);\n+\n+            if (index < 0) {\n+                throw new IllegalStateException(\"Tried to split a segment where the node doesn't exist in this segment\");\n+            } else if (index == this.visited.size()-1) { // We forked just off the most recent node\n+                newFork.following.add(this);\n+                newFork.following.add(joiningBranch);\n+                this.visited.remove(index);\n+            } else if (index == 0) {\n+                throw new IllegalStateException(\"We have a cyclic graph or heads that are not separate branches!\");\n+            } else { // Splitting at some midpoint within the segment, everything before becomes part of the following\n+                // Execute the split: create a new fork at the fork point, and shuffle the part of the flow after it\n+                //   to a new segment and add that to the fork.\n+\n+                FlowSegment newSegment = new FlowSegment();\n+                newSegment.after = this.after;\n+                newSegment.visited.addAll(this.visited.subList(0, index));\n+                newFork.following.add(newSegment);\n+                newFork.following.add(joiningBranch);\n+                this.after = newFork;\n+                this.isLeaf = false;\n+\n+                // Remove the part before the fork point\n+                this.visited.subList(0, index+1).clear();\n+                for (FlowNode n : newSegment.visited) {\n+                    nodeMapping.put(n, newSegment);\n+                }\n+            }\n+            nodeMapping.put(joinPoint, newFork);\n+            return newFork;\n+        }\n+\n+        public void add(FlowNode f) {\n+            this.visited.add(f);\n+        }\n+    }\n+\n+    /** Internal class used for constructing the LeastCommonAncestor structure */\n+    static class Fork extends ParallelBlockStart implements FlowPiece {\n+        List<FlowPiece> following = new ArrayList<FlowPiece>();\n+\n+        @Override\n+        public boolean isLeaf() {\n+            return false;\n+        }\n+\n+        public Fork(BlockStartNode forkNode) {\n+            this.forkStart = forkNode;\n+        }\n+    }\n+\n+    /** Does a conversion of the fork container class to a set of block starts */\n+    ArrayDeque<ParallelBlockStart> convertForksToBlockStarts(ArrayDeque<Fork> parallelForks) {\n+        // Walk through and convert forks to parallel block starts, and find heads that point to them\n+        ArrayDeque<ParallelBlockStart> output = new ArrayDeque<ParallelBlockStart>();\n+        for (Fork f : parallelForks) {\n+            // Do processing to assign heads to flowsegments\n+            ParallelBlockStart start = new ParallelBlockStart();\n+            start.totalBranches = f.following.size();\n+            start.forkStart = f.forkStart;\n+            start.remainingBranches = start.totalBranches;\n+            start.unvisited = new ArrayDeque<FlowNode>();\n+\n+            // Add the nodes to the parallel starts here\n+            for (FlowPiece fp : f.following) {\n+                if (fp.isLeaf()) { // Forks are never leaves\n+                    start.unvisited.add(((FlowSegment)fp).visited.get(0));\n+                }\n+            }\n+            output.add(start);\n+        }\n+        return output;\n+    }\n+\n+    /**\n+     * Create the necessary information about parallel blocks in order to provide flowscanning from inside incomplete parallel branches\n+     * This works by walking back to construct the tree of parallel blocks covering all heads back to the Least Common Ancestor of all heads\n+     *  (the top parallel block).  One by one, as branches join, we remove them from the list of live pieces and replace with their common ancestor.\n+     *\n+     * <p/> The core algorithm is simple in theory but the many cases render the implementation quite complex. In gist:\n+     * <ul>\n+     *     <li>We track FlowPieces, which are Forks (where branches merge) and FlowSegments (where there's a unforked sequence of nodes)</li>\n+     *     <li>A map of FlowNode to its containing FlowPiece is created </li>\n+     *     <li>For each head we start a new FlowSegment and create an iterator of all enclosing blocks (all we need for this)</li>\n+     *     <li>We do a series of passes through all iterators looking to see if the parent of any given piece maps to an existing FlowPiece</li>\n+     *     <ol>\n+     *         <li>Where there are no mappings, we add another node to the FlowSegment</li>\n+     *         <li>Where an existing piece exists, <strong>if it's a Fork</strong>, we add the current piece on as a new branch</li>\n+     *         <li>Where an existing piece exists <strong>if it's a FlowSegment</strong>, we create a fork:\n+     *              <ul><li>If we're joining at the most recent point, create a Fork with both branches following it, and replace that item's ForkSegment in the piece list with a Fork</li>\n+     *              <li>If joining midway through, split the segment and create a fork as needed</li></ul>\n+     *         </li>\n+     *         <li>When two pieces join together, we remove one from the list</li>\n+     *         <li>When we're down to a single piece, we have the full ancestry & we're done</li>\n+     *         <li>When we're down to a single piece, all heads have merged and we're done</li>\n+     *     </ol>\n+     *     <li>Each time we merge a branch in, we need to remove an entry from enclosing blocks & live pieces</li>\n+     * </ul>\n+     *\n+     * <p/>  There are some assumptions you need to know about to understand why this works:\n+     * <ul>\n+     *     <li>None of the pieces have multiple parents, since we only look at enclosing blocks (only be a BlockEndNodes for a parallel block have multipel parents)</li>\n+     *     <li>No cycles exist in the graph</li>\n+     *     <li>Flow graphs are correctly constructed</li>\n+     *     <li>Heads are all separate branches</li>\n+     * </ul>\n+     *\n+     * @param heads\n+     */\n+    ArrayDeque<ParallelBlockStart> leastCommonAncestor(@Nonnull Set<FlowNode> heads) {\n+        HashMap<FlowNode, FlowPiece> branches = new HashMap<FlowNode, FlowPiece>();\n+        ArrayList<Filterator<FlowNode>> iterators = new ArrayList<Filterator<FlowNode>>();\n+        ArrayList<FlowPiece> livePieces = new ArrayList<FlowPiece>();\n+\n+        ArrayDeque<Fork> parallelForks = new ArrayDeque<Fork>();  // Tracks the discovered forks in order of encounter\n+\n+        for (FlowNode f : heads) {\n+            iterators.add(FlowScanningUtils.filterableEnclosingBlocks(f));\n+            FlowSegment b = new FlowSegment();\n+            b.add(f);\n+            livePieces.add(b);\n+            branches.put(f, b);\n+        }\n+\n+        // Walk through, merging flownodes one-by-one until everything has merged to one ancestor\n+        while (iterators.size() > 1) {\n+            ListIterator<Filterator<FlowNode>> itIterator = iterators.listIterator();\n+            ListIterator<FlowPiece> pieceIterator = livePieces.listIterator();\n+\n+            while (itIterator.hasNext()) {\n+                Filterator<FlowNode> blockStartIterator = itIterator.next();\n+                FlowPiece myPiece = pieceIterator.next();\n+\n+                // Welp we hit the end of a branch\n+                if (!blockStartIterator.hasNext()) {\n+                    pieceIterator.remove();\n+                    itIterator.remove();\n+                    continue;\n+                }\n+\n+                FlowNode nextBlockStart = blockStartIterator.next();\n+\n+                // Look for cases where two branches merge together\n+                FlowPiece existingPiece = branches.get(nextBlockStart);\n+                if (existingPiece == null && myPiece instanceof FlowSegment) { // No merge, just add to segment\n+                    ((FlowSegment) myPiece).add(nextBlockStart);\n+                    branches.put(nextBlockStart, myPiece);\n+                } else if (existingPiece == null && myPiece instanceof Fork) {  // No merge, we had a fork. Start a segment preceding the fork\n+                    FlowSegment newSegment = new FlowSegment();\n+                    newSegment.isLeaf = false;\n+                    newSegment.add(nextBlockStart);\n+                    newSegment.after = myPiece;\n+                    pieceIterator.remove();\n+                    pieceIterator.add(newSegment);\n+                    branches.put(nextBlockStart, newSegment);\n+                } else if (existingPiece != null) {  // Always not null. We're merging into another thing, we're going to elliminate a branch\n+                    if (existingPiece instanceof Fork) {\n+                        ((Fork) existingPiece).following.add(myPiece);\n+                    } else { // Split a flow segment so it forks against this one\n+                        Fork f = ((FlowSegment) existingPiece).split(branches, (BlockStartNode)nextBlockStart, myPiece);\n+                        // If we split the existing segment at its end, we created a fork replacing its latest node\n+                        // Thus we must replace the piece with the fork ahead of it\n+                        if (f.following.contains(existingPiece) ) {\n+                            int headIndex = livePieces.indexOf(existingPiece);\n+                            livePieces.set(headIndex, f);\n+                        }\n+                        parallelForks.add(f);\n+                    }\n+\n+                    // Merging removes the piece & its iterator from heads\n+                    itIterator.remove();\n+                    pieceIterator.remove();\n+                }\n+            }\n+        }\n+\n+        // If we hit issues with the ordering of blocks by depth, apply a sorting to the parallels by depth\n+        return convertForksToBlockStarts(parallelForks);\n+    }\n+\n+    @Override\n+    protected void setHeads(@Nonnull Collection<FlowNode> heads) {\n+        if (heads.size() > 1) {\n+            //throw new IllegalArgumentException(\"ForkedFlowScanner can't handle multiple head nodes yet\");\n+            parallelBlockStartStack = leastCommonAncestor(new LinkedHashSet<FlowNode>(heads));\n+            currentParallelStart = parallelBlockStartStack.pop();","path":"src/main/java/org/jenkinsci/plugins/workflow/graphanalysis/ForkScanner.java","position":317,"original_position":317,"commit_id":"606de645da9077544ffb404ff0de535069079ba4","original_commit_id":"606de645da9077544ffb404ff0de535069079ba4","user":{"login":"oleg-nenashev","id":3000480,"avatar_url":"https://avatars.githubusercontent.com/u/3000480?v=3","gravatar_id":"","url":"https://api.github.com/users/oleg-nenashev","html_url":"https://github.com/oleg-nenashev","followers_url":"https://api.github.com/users/oleg-nenashev/followers","following_url":"https://api.github.com/users/oleg-nenashev/following{/other_user}","gists_url":"https://api.github.com/users/oleg-nenashev/gists{/gist_id}","starred_url":"https://api.github.com/users/oleg-nenashev/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/oleg-nenashev/subscriptions","organizations_url":"https://api.github.com/users/oleg-nenashev/orgs","repos_url":"https://api.github.com/users/oleg-nenashev/repos","events_url":"https://api.github.com/users/oleg-nenashev/events{/privacy}","received_events_url":"https://api.github.com/users/oleg-nenashev/received_events","type":"User","site_admin":false},"body":"And what if we have an empty parallel block?","created_at":"2016-08-22T11:10:33Z","updated_at":"2016-08-22T11:10:33Z","html_url":"https://github.com/jenkinsci/workflow-api-plugin/pull/2#discussion_r75658707","pull_request_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/pulls/2","_links":{"self":{"href":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/pulls/comments/75658707"},"html":{"href":"https://github.com/jenkinsci/workflow-api-plugin/pull/2#discussion_r75658707"},"pull_request":{"href":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/pulls/2"}}},"pull_request":{"url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/pulls/2","id":66425394,"html_url":"https://github.com/jenkinsci/workflow-api-plugin/pull/2","diff_url":"https://github.com/jenkinsci/workflow-api-plugin/pull/2.diff","patch_url":"https://github.com/jenkinsci/workflow-api-plugin/pull/2.patch","issue_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/issues/2","number":2,"state":"open","locked":false,"title":"Generified Flow Search Algorithms","user":{"login":"svanoort","id":5400948,"avatar_url":"https://avatars.githubusercontent.com/u/5400948?v=3","gravatar_id":"","url":"https://api.github.com/users/svanoort","html_url":"https://github.com/svanoort","followers_url":"https://api.github.com/users/svanoort/followers","following_url":"https://api.github.com/users/svanoort/following{/other_user}","gists_url":"https://api.github.com/users/svanoort/gists{/gist_id}","starred_url":"https://api.github.com/users/svanoort/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/svanoort/subscriptions","organizations_url":"https://api.github.com/users/svanoort/orgs","repos_url":"https://api.github.com/users/svanoort/repos","events_url":"https://api.github.com/users/svanoort/events{/privacy}","received_events_url":"https://api.github.com/users/svanoort/received_events","type":"User","site_admin":false},"body":"Provides a bundle of search and iteration APIs to simplify working with pipeline flow graphs (DAGs). \r\n\r\nThese are intended to be extensible for custom use cases, but provide a general API that covers what is needed. \r\n\r\nFor a rundown of the core APIs and how to work with them, see: https://github.com/jenkinsci/workflow-api-plugin/pull/2/files#diff-f8537b8acb35b350992eb264d4f9f207R43\r\n\r\nIn gist, we're offering some core primitives for working with the flow graph that are needed to do more involved work with the flow graph:\r\n\r\n* The visitor pattern is the starting point for SAX-like analysis of a flowgraph (and may be extended to provide a block-aware or marker-aware, i.e. stage/milestone-aware visitor)\r\n* ForkScanner is needed to effectively handle blocks in the flowgraph (since blocks are seen in a logical linear order).  It is the primitive needed for block-based use in a DOM-like tree analysis, or block-aware SAX-like analysis\r\n* LinearBlockHoppingFlowScanner is the primitive that allows for quickly ignoring sibling blocks (helpful if you only care about preceding or enclosing nodes, similar to the FlowNodeSerialWalker). \r\n\r\nNotes:\r\n\r\n* Parallel blocks have their own LabelAction for each branch, a ParallelLabelAction on the StepStartNode beginning each branch, with displayname branch + displayname, and getThreadName that returns the label given to the branch, StepEndNode gives the timing too (and final state)\r\n\r\nGeneral use cases:\r\n* ErrorInfo - https://github.com/jenkinsci/workflow-basic-steps-plugin/pull/2 = https://issues.jenkins-ci.org/browse/JENKINS-28119\r\n   - Searching back for node emitting failure (ErrorAction), skips block end node because the blockendnode gets the final error action.\r\n  - Then does the walk back via blockjumping to get the nearest log action, and the nearest label too\r\n* [`WorkflowRun.getLogPrefix`](https://github.com/jenkinsci/workflow-job-plugin/blob/master/src/main/java/org/jenkinsci/plugins/workflow/job/WorkflowRun.java#L402-L426).\r\n* [Milestone step - JENKINS-27039](https://github.com/jenkinsci/pipeline-milestone-step-plugin/pull/1)\r\n  - Needs something like FlowGraphWalker but that bombs out on parallel -> optimal case for recursive breadth-first algorithm (planned but not implemented yet).  Caching not allowable probably.\r\n* Durable task step: search label/stage which uses a executor node.\r\n   - Good use case for caching or incremental caching. \r\n* Log search: nearest enclose start node with label for parallel\r\n\r\nPipeline Stage View:\r\n * Stage/node labels - the BlockJumpingScanner allows us to solve [JENKINS-33290](https://issues.jenkins-ci.org/browse/JENKINS-33290)","created_at":"2016-04-14T03:47:14Z","updated_at":"2016-08-22T11:10:33Z","closed_at":null,"merged_at":null,"merge_commit_sha":"3d97568774d4b1910dfb9bfb1c2a1ab81314ff54","assignee":null,"assignees":[],"milestone":null,"commits_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/pulls/2/commits","review_comments_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/pulls/2/comments","review_comment_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/pulls/comments{/number}","comments_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/issues/2/comments","statuses_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/statuses/606de645da9077544ffb404ff0de535069079ba4","head":{"label":"jenkinsci:generic-flow-analyzer","ref":"generic-flow-analyzer","sha":"606de645da9077544ffb404ff0de535069079ba4","user":{"login":"jenkinsci","id":107424,"avatar_url":"https://avatars.githubusercontent.com/u/107424?v=3","gravatar_id":"","url":"https://api.github.com/users/jenkinsci","html_url":"https://github.com/jenkinsci","followers_url":"https://api.github.com/users/jenkinsci/followers","following_url":"https://api.github.com/users/jenkinsci/following{/other_user}","gists_url":"https://api.github.com/users/jenkinsci/gists{/gist_id}","starred_url":"https://api.github.com/users/jenkinsci/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jenkinsci/subscriptions","organizations_url":"https://api.github.com/users/jenkinsci/orgs","repos_url":"https://api.github.com/users/jenkinsci/repos","events_url":"https://api.github.com/users/jenkinsci/events{/privacy}","received_events_url":"https://api.github.com/users/jenkinsci/received_events","type":"Organization","site_admin":false},"repo":{"id":55527408,"name":"workflow-api-plugin","full_name":"jenkinsci/workflow-api-plugin","owner":{"login":"jenkinsci","id":107424,"avatar_url":"https://avatars.githubusercontent.com/u/107424?v=3","gravatar_id":"","url":"https://api.github.com/users/jenkinsci","html_url":"https://github.com/jenkinsci","followers_url":"https://api.github.com/users/jenkinsci/followers","following_url":"https://api.github.com/users/jenkinsci/following{/other_user}","gists_url":"https://api.github.com/users/jenkinsci/gists{/gist_id}","starred_url":"https://api.github.com/users/jenkinsci/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jenkinsci/subscriptions","organizations_url":"https://api.github.com/users/jenkinsci/orgs","repos_url":"https://api.github.com/users/jenkinsci/repos","events_url":"https://api.github.com/users/jenkinsci/events{/privacy}","received_events_url":"https://api.github.com/users/jenkinsci/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/jenkinsci/workflow-api-plugin","description":"","fork":false,"url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin","forks_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/forks","keys_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/keys{/key_id}","collaborators_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/teams","hooks_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/hooks","issue_events_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/issues/events{/number}","events_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/events","assignees_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/assignees{/user}","branches_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/branches{/branch}","tags_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/tags","blobs_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/git/refs{/sha}","trees_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/git/trees{/sha}","statuses_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/statuses/{sha}","languages_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/languages","stargazers_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/stargazers","contributors_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/contributors","subscribers_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/subscribers","subscription_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/subscription","commits_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/commits{/sha}","git_commits_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/git/commits{/sha}","comments_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/comments{/number}","issue_comment_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/issues/comments{/number}","contents_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/contents/{+path}","compare_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/compare/{base}...{head}","merges_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/merges","archive_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/downloads","issues_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/issues{/number}","pulls_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/pulls{/number}","milestones_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/milestones{/number}","notifications_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/labels{/name}","releases_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/releases{/id}","deployments_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/deployments","created_at":"2016-04-05T17:18:43Z","updated_at":"2016-06-16T14:15:12Z","pushed_at":"2016-08-21T19:47:34Z","git_url":"git://github.com/jenkinsci/workflow-api-plugin.git","ssh_url":"git@github.com:jenkinsci/workflow-api-plugin.git","clone_url":"https://github.com/jenkinsci/workflow-api-plugin.git","svn_url":"https://github.com/jenkinsci/workflow-api-plugin","homepage":"https://wiki.jenkins-ci.org/display/JENKINS/Pipeline+API+Plugin","size":457,"stargazers_count":1,"watchers_count":1,"language":"Java","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":8,"mirror_url":null,"open_issues_count":4,"forks":8,"open_issues":4,"watchers":1,"default_branch":"master"}},"base":{"label":"jenkinsci:master","ref":"master","sha":"5a35dd0a30b7ef6b283c6b0c9d1bf513d33cced2","user":{"login":"jenkinsci","id":107424,"avatar_url":"https://avatars.githubusercontent.com/u/107424?v=3","gravatar_id":"","url":"https://api.github.com/users/jenkinsci","html_url":"https://github.com/jenkinsci","followers_url":"https://api.github.com/users/jenkinsci/followers","following_url":"https://api.github.com/users/jenkinsci/following{/other_user}","gists_url":"https://api.github.com/users/jenkinsci/gists{/gist_id}","starred_url":"https://api.github.com/users/jenkinsci/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jenkinsci/subscriptions","organizations_url":"https://api.github.com/users/jenkinsci/orgs","repos_url":"https://api.github.com/users/jenkinsci/repos","events_url":"https://api.github.com/users/jenkinsci/events{/privacy}","received_events_url":"https://api.github.com/users/jenkinsci/received_events","type":"Organization","site_admin":false},"repo":{"id":55527408,"name":"workflow-api-plugin","full_name":"jenkinsci/workflow-api-plugin","owner":{"login":"jenkinsci","id":107424,"avatar_url":"https://avatars.githubusercontent.com/u/107424?v=3","gravatar_id":"","url":"https://api.github.com/users/jenkinsci","html_url":"https://github.com/jenkinsci","followers_url":"https://api.github.com/users/jenkinsci/followers","following_url":"https://api.github.com/users/jenkinsci/following{/other_user}","gists_url":"https://api.github.com/users/jenkinsci/gists{/gist_id}","starred_url":"https://api.github.com/users/jenkinsci/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jenkinsci/subscriptions","organizations_url":"https://api.github.com/users/jenkinsci/orgs","repos_url":"https://api.github.com/users/jenkinsci/repos","events_url":"https://api.github.com/users/jenkinsci/events{/privacy}","received_events_url":"https://api.github.com/users/jenkinsci/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/jenkinsci/workflow-api-plugin","description":"","fork":false,"url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin","forks_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/forks","keys_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/keys{/key_id}","collaborators_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/teams","hooks_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/hooks","issue_events_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/issues/events{/number}","events_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/events","assignees_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/assignees{/user}","branches_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/branches{/branch}","tags_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/tags","blobs_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/git/refs{/sha}","trees_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/git/trees{/sha}","statuses_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/statuses/{sha}","languages_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/languages","stargazers_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/stargazers","contributors_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/contributors","subscribers_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/subscribers","subscription_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/subscription","commits_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/commits{/sha}","git_commits_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/git/commits{/sha}","comments_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/comments{/number}","issue_comment_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/issues/comments{/number}","contents_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/contents/{+path}","compare_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/compare/{base}...{head}","merges_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/merges","archive_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/downloads","issues_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/issues{/number}","pulls_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/pulls{/number}","milestones_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/milestones{/number}","notifications_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/labels{/name}","releases_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/releases{/id}","deployments_url":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/deployments","created_at":"2016-04-05T17:18:43Z","updated_at":"2016-06-16T14:15:12Z","pushed_at":"2016-08-21T19:47:34Z","git_url":"git://github.com/jenkinsci/workflow-api-plugin.git","ssh_url":"git@github.com:jenkinsci/workflow-api-plugin.git","clone_url":"https://github.com/jenkinsci/workflow-api-plugin.git","svn_url":"https://github.com/jenkinsci/workflow-api-plugin","homepage":"https://wiki.jenkins-ci.org/display/JENKINS/Pipeline+API+Plugin","size":457,"stargazers_count":1,"watchers_count":1,"language":"Java","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":8,"mirror_url":null,"open_issues_count":4,"forks":8,"open_issues":4,"watchers":1,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/pulls/2"},"html":{"href":"https://github.com/jenkinsci/workflow-api-plugin/pull/2"},"issue":{"href":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/issues/2"},"comments":{"href":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/issues/2/comments"},"review_comments":{"href":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/pulls/2/comments"},"review_comment":{"href":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/pulls/2/commits"},"statuses":{"href":"https://api.github.com/repos/jenkinsci/workflow-api-plugin/statuses/606de645da9077544ffb404ff0de535069079ba4"}}}}
{"actor":{"display_login":"oleg-nenashev"}}