{"action":"created","comment":{"url":"https://api.github.com/repos/pantsbuild/pants/pulls/comments/205120219","pull_request_review_id":140259125,"id":205120219,"node_id":"MDI0OlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudDIwNTEyMDIxOQ==","diff_hunk":"@@ -251,115 +322,472 @@ impl<'t> GraphMaker<'t> {\n   }\n \n   ///\n-  /// Computes (and memoizes) whether any rules can compute the given `product_type` for the given\n-  /// `subject_type`.\n+  /// Computes whether the given candidate Entry is satisfiable, and if it is, returns a copy\n+  /// of the Entry for each set of input parameters that will satisfy it. Once computed, the\n+  /// simplified versions are memoized in all_simplified_entries.\n   ///\n-  /// When a rule cannot be fulfilled, it is added to `unfulfillable_rules` rather than to\n-  /// `rule_dependency_edges`.\n+  /// When a rule can be fulfilled it will end up stored in both the rule_dependency_edges and\n+  /// all_simplified_entries. If it can't be fulfilled, it is added to `unfulfillable_rules`.\n   ///\n-  fn _construct_graph_helper(\n+  fn construct_graph_helper(\n     &self,\n     rule_dependency_edges: &mut RuleDependencyEdges,\n+    all_simplified_entries: &mut HashMap<EntryWithDeps, Vec<EntryWithDeps>>,\n     unfulfillable_rules: &mut UnfulfillableRuleMap,\n     entry: EntryWithDeps,\n-  ) -> bool {\n-    // If the entry has not been visited before, store a placeholder in the unfulfillable rules map\n-    // and then visit its children. Otherwise, we're done.\n+  ) -> ConstructGraphResult {\n+    if let Some(simplified) = all_simplified_entries.get(&entry) {\n+      // A simplified equivalent entry has already been computed, return it.\n+      return ConstructGraphResult::Fulfilled(simplified.clone());\n+    } else if let Some(_) = unfulfillable_rules.get(&entry) {\n+      // The rule is unfulfillable.\n+      return ConstructGraphResult::Unfulfillable;\n+    }\n+\n+    // Otherwise, store a placeholder in the rule_dependency_edges map and then visit its\n+    // children.\n     //\n     // This prevents infinite recursion by shortcircuiting when an entry recursively depends on\n     // itself. It's totally fine for rules to be recursive: the recursive path just never\n     // contributes to whether the rule is satisfiable.\n-    match (unfulfillable_rules.entry(entry.clone()), rule_dependency_edges.entry(entry.clone())) {\n-      (hash_map::Entry::Vacant(_), hash_map::Entry::Vacant(re)) => {\n-        // When a rule has not been visited before, we visit it by storing a placeholder in the\n-        // rule dependencies map (to prevent infinite recursion).\n+    match rule_dependency_edges.entry(entry.clone()) {\n+      hash_map::Entry::Vacant(re) => {\n+        // When a rule has not been visited before, we start the visit by storing a placeholder in\n+        // the rule dependencies map in order to detect rule cycles.\n         re.insert(RuleEdges::default());\n-      },\n-      (hash_map::Entry::Vacant(_), hash_map::Entry::Occupied(_)) =>\n-        // Rule has been visited before and been found to be valid, or is currently being\n-        // recursively visited and has a placeholder.\n-        return true,\n-      (hash_map::Entry::Occupied(_), _) =>\n-        // Rule has either been visited before and found unfulfillable.\n-        return false,\n+      }\n+      hash_map::Entry::Occupied(_) => {\n+        // We're currently recursively under this rule, but its simplified equivalence has not yet\n+        // been computed (or we would have returned it above). The cyclic parent(s) will complete\n+        // before recursing to compute this node again.\n+        let mut cyclic_deps = HashSet::new();\n+        cyclic_deps.insert(entry.clone());\n+        return ConstructGraphResult::CycledOn {\n+          cyclic_deps,\n+          partial_simplified_entries: vec![entry.simplified(BTreeSet::new())],\n+        };\n+      }\n     };\n \n-    // For each dependency of the rule, recurse for each potential match and collect RuleEdges.\n-    let mut edges = RuleEdges::new();\n-    let mut fulfillable = true;\n-    for select_key in entry.dependency_keys() {\n-      let (subject, product) = match &select_key {\n-        &SelectKey::JustSelect(ref s) => (entry.subject_type(), s.product),\n-        &SelectKey::JustGet(ref g) => (g.subject, g.product),\n+    // For each dependency of the rule, recurse for each potential match and collect RuleEdges and\n+    // used parameters.\n+    //\n+    // This is a `loop` because if we discover that this entry needs to complete in order to break\n+    // a cycle on itself, it will re-compute dependencies after having partially-completed.\n+    loop {\n+      if let Ok(res) = self.construct_dependencies(\n+        rule_dependency_edges,\n+        all_simplified_entries,\n+        unfulfillable_rules,\n+        entry.clone(),\n+      ) {\n+        break res;\n+      }\n+    }\n+  }\n+\n+  ///\n+  /// For each dependency of the rule, recurse for each potential match and collect RuleEdges and\n+  /// used parameters.\n+  ///\n+  /// This is called in a `loop` until it succeeds, because if we discover that this entry needs\n+  /// to complete in order to break a cycle on itself, it will re-compute dependencies after having\n+  /// partially-completed.\n+  ///\n+  fn construct_dependencies(\n+    &self,\n+    rule_dependency_edges: &mut RuleDependencyEdges,\n+    all_simplified_entries: &mut HashMap<EntryWithDeps, Vec<EntryWithDeps>>,\n+    unfulfillable_rules: &mut UnfulfillableRuleMap,\n+    entry: EntryWithDeps,\n+  ) -> Result<ConstructGraphResult, ()> {\n+    let mut fulfillable_candidates_by_key = HashMap::new();\n+    let mut cycled_on = HashSet::new();\n+    let mut unfulfillable_diagnostics = Vec::new();\n+\n+    let (dependency_keys, is_aggregation) = entry.dependency_keys();\n+\n+    for select_key in dependency_keys {\n+      let (params, product) = match &select_key {\n+        &SelectKey::JustSelect(ref s) => (entry.params().clone(), s.product.clone()),\n+        &SelectKey::JustGet(ref g) => {\n+          let get_params = {\n+            let mut p = entry.params().clone();\n+            p.insert(g.subject.clone());\n+            p\n+          };\n+          (get_params, g.product.clone())\n+        }\n       };\n \n-      // Confirm that at least one candidate is fulfillable.\n-      let fulfillable_candidates = rhs(&self.tasks, subject, &product)\n-        .into_iter()\n-        .filter(|candidate| match candidate {\n-          &Entry::WithDeps(ref c) => {\n-            self._construct_graph_helper(rule_dependency_edges, unfulfillable_rules, c.clone())\n+      // Collect fulfillable candidates, used parameters, and cyclic deps.\n+      let mut cycled = false;\n+      let mut fulfillable_candidates = fulfillable_candidates_by_key\n+        .entry(select_key.clone())\n+        .or_insert_with(Vec::new);\n+      for candidate in rhs(&self.tasks, &params, &product, is_aggregation) {\n+        match candidate {\n+          Entry::WithDeps(c) => match self.construct_graph_helper(\n+            rule_dependency_edges,\n+            all_simplified_entries,\n+            unfulfillable_rules,\n+            c.clone(),\n+          ) {\n+            ConstructGraphResult::Unfulfillable => {}\n+            ConstructGraphResult::Fulfilled(simplified_entries) => {\n+              fulfillable_candidates.push(\n+                simplified_entries\n+                  .into_iter()\n+                  .map(Entry::WithDeps)\n+                  .collect::<Vec<_>>(),\n+              );\n+            }\n+            ConstructGraphResult::CycledOn {\n+              cyclic_deps,\n+              partial_simplified_entries,\n+            } => {\n+              cycled = true;\n+              cycled_on.extend(cyclic_deps);\n+              fulfillable_candidates.push(\n+                partial_simplified_entries\n+                  .into_iter()\n+                  .map(Entry::WithDeps)\n+                  .collect::<Vec<_>>(),\n+              );\n+            }\n+          },\n+          p @ Entry::Param(_) => {\n+            fulfillable_candidates.push(vec![p]);\n           }\n-          &Entry::SubjectIsProduct { .. } => true,\n-          &Entry::Singleton { .. } => true,\n-        })\n-        .collect::<Vec<_>>();\n+          s @ Entry::Singleton { .. } => {\n+            fulfillable_candidates.push(vec![s]);\n+          }\n+        };\n+      }\n \n-      if fulfillable_candidates.is_empty() {\n+      if cycled {\n+        // If any candidate triggered a cycle on a rule that has not yet completed, then we are not\n+        // yet fulfillable, and should finish gathering any other cyclic rule dependencies.\n+        continue;\n+      } else if fulfillable_candidates.is_empty() {\n         // If no candidates were fulfillable, this rule is not fulfillable.\n-        unfulfillable_rules\n-          .entry(entry.clone())\n-          .or_insert_with(Vec::new)\n-          .push(Diagnostic {\n-            subject_type: subject,\n-            reason: format!(\n-              \"no rule was available to compute {} for subject type {}\",\n-              type_constraint_str(product),\n-              type_str(subject)\n-            ),\n-          });\n-        fulfillable = false;\n-      } else {\n-        // Extend the RuleEdges for this SelectKey.\n-        edges.add_edges_via(select_key, fulfillable_candidates);\n+        unfulfillable_diagnostics.push(Diagnostic {\n+          params: params.clone(),\n+          reason: format!(\n+            \"no rule was available to compute {} with parameter type{} {}\",\n+            type_constraint_str(product.clone()),\n+            if params.len() > 1 { \"s\" } else { \"\" },\n+            params_str(&params),\n+          ),\n+          details: vec![],\n+        });\n       }\n     }\n \n-    if fulfillable {\n-      // All depedendencies were fulfillable: replace the placeholder with the computed RuleEdges.\n-      rule_dependency_edges.insert(entry, edges);\n-      true\n-    } else {\n+    // If any dependencies were completely unfulfillable, then whether or not there were cyclic\n+    // dependencies isn't relevant.\n+    if !unfulfillable_diagnostics.is_empty() {\n       // Was not fulfillable. Remove the placeholder: the unfulfillable entries we stored will\n       // prevent us from attempting to expand this node again.\n+      unfulfillable_rules\n+        .entry(entry.clone())\n+        .or_insert_with(Vec::new)\n+        .extend(unfulfillable_diagnostics);\n       rule_dependency_edges.remove(&entry);\n-      false\n+      return Ok(ConstructGraphResult::Unfulfillable);\n+    }\n+\n+    // No dependencies were completely unfulfillable (although some may have been cyclic).\n+    //\n+    // If this is an Aggregration, flatten the candidates by duplicating the SelectKey to treat\n+    // each concrete rule as a group of candidates. Otherwise, flatten each group of candidates.\n+    let flattened_fulfillable_candidates_by_key = if is_aggregation {\n+      // Expect one key. TODO: Adjust the `dependency_keys` method to make this typesafe.\n+      let (key, fulfillable_candidates): (SelectKey, Vec<Vec<Entry>>) =\n+        fulfillable_candidates_by_key.into_iter().next().unwrap();\n+      // Treat each group of candidates as an input, all with the same key.\n+      // TODO: RuleEdges will need adjustment to support this duplication of SelectKey.\n+      fulfillable_candidates\n+        .into_iter()\n+        .map(|candidates| (key.clone(), candidates))\n+        .collect()\n+    } else {\n+      fulfillable_candidates_by_key\n+        .into_iter()\n+        .map(|(k, candidate_group)| (k, Itertools::flatten(candidate_group.into_iter()).collect()))\n+        .collect()\n+    };\n+\n+    // Generate one Entry per legal combination of parameters.\n+    let simplified_entries =\n+      match Self::monomorphize(&entry, flattened_fulfillable_candidates_by_key) {\n+        Ok(se) => se,\n+        Err(ambiguous_diagnostics) => {\n+          // At least one combination of the dependencies was ambiguous.\n+          unfulfillable_rules\n+            .entry(entry.clone())\n+            .or_insert_with(Vec::new)\n+            .extend(ambiguous_diagnostics);\n+          rule_dependency_edges.remove(&entry);\n+          return Ok(ConstructGraphResult::Unfulfillable);\n+        }\n+      };\n+    let simplified_entries_only = simplified_entries\n+      .iter()\n+      .map(|(entry, _)| entry.clone())\n+      .collect();\n+\n+    if !cycled_on.is_empty() {\n+      // The set of cycled dependencies can only contain call stack \"parents\" of the dependency: we\n+      // remove this entry from the set (if we're in it), until the top-most cyclic parent\n+      // (represented by an empty set) is the one that re-starts recursion.\n+      cycled_on.remove(&entry);\n+      if cycled_on.is_empty() {\n+        // If we were the only member of the set of cyclic dependencies, then we are the top-most\n+        // cyclic parent in the call stack, and we should complete. This represents the case where\n+        // a rule recursively depends on itself, and thus \"cannot complete without completing\".\n+        //\n+        // Store our simplified equivalence and then re-execute our dependency discovery. In this\n+        // second attempt our cyclic dependencies will use the simplified representation(s) to succeed.\n+        all_simplified_entries.insert(entry.clone(), simplified_entries_only);\n+        return Err(());\n+      } else {\n+        // This rule may be fulfillable, but we can't compute its complete set of dependencies until\n+        // parent rule entries complete. Remove our placeholder edges before returning.\n+        rule_dependency_edges.remove(&entry);\n+        return Ok(ConstructGraphResult::CycledOn {\n+          cyclic_deps: cycled_on,\n+          partial_simplified_entries: simplified_entries_only,\n+        });\n+      }\n+    } else {\n+      // All dependencies were fulfillable and none were blocked on cycles. Remove the\n+      // placeholder and store the simplified entries.\n+      rule_dependency_edges.remove(&entry);\n+      rule_dependency_edges.extend(simplified_entries);\n+\n+      all_simplified_entries.insert(entry.clone(), simplified_entries_only.clone());\n+      return Ok(ConstructGraphResult::Fulfilled(simplified_entries_only));\n     }\n   }\n \n-  fn gen_root_entries(&self, product_types: &HashSet<TypeConstraint>) -> Vec<RootEntry> {\n-    let mut result: Vec<RootEntry> = Vec::new();\n-    for subj_type in &self.root_subject_types {\n-      for pt in product_types {\n-        if let Some(entry) = self.gen_root_entry(*subj_type, pt) {\n-          result.push(entry);\n+  ///\n+  /// Given an Entry and a mapping of all legal sources of each of its dependencies, generates a\n+  /// simplified Entry for each legal combination of parameters.\n+  ///\n+  /// Computes the union of all parameters used by the dependencies, and then uses the powerset of\n+  /// used parameters to filter the possible combinations of dependencies. If multiple choices of\n+  /// dependencies are possible for any set of parameters, then the graph is ambiguous.\n+  ///\n+  fn monomorphize(\n+    entry: &EntryWithDeps,\n+    deps: Vec<(SelectKey, Vec<Entry>)>,\n+  ) -> Result<HashMap<EntryWithDeps, RuleEdges>, Vec<Diagnostic>> {\n+    // Collect the powerset of the union of used parameters, ordered by set size.\n+    let params_powerset: Vec<Vec<TypeId>> = {\n+      let mut all_used_params = BTreeSet::new();\n+      for (key, inputs) in &deps {\n+        for input in inputs {\n+          all_used_params.extend(Self::used_params(key, input));\n         }\n       }\n+      // Compute the powerset ordered by ascending set size.\n+      let all_used_params = all_used_params.into_iter().collect::<Vec<_>>();\n+      let mut param_sets = Self::powerset(&all_used_params).collect::<Vec<_>>();\n+      param_sets.sort_by(|l, r| l.len().cmp(&r.len()));\n+      param_sets\n+    };\n+\n+    // Then, for the powerset of used parameters, determine which dependency combinations are\n+    // satisfiable.\n+    let mut combinations: HashMap<EntryWithDeps, _> = HashMap::new();\n+    let mut diagnostics = Vec::new();\n+    for available_params in params_powerset {\n+      let available_params = available_params.into_iter().collect();\n+      // If a subset of these parameters is already satisfied, skip. This has the effect of","path":"src/rust/engine/src/rule_graph.rs","position":665,"original_position":665,"commit_id":"438e607a8519f9b3d8a31195fe03050e070af0a1","original_commit_id":"438e607a8519f9b3d8a31195fe03050e070af0a1","user":{"login":"illicitonion","id":1131704,"node_id":"MDQ6VXNlcjExMzE3MDQ=","avatar_url":"https://avatars0.githubusercontent.com/u/1131704?v=4","gravatar_id":"","url":"https://api.github.com/users/illicitonion","html_url":"https://github.com/illicitonion","followers_url":"https://api.github.com/users/illicitonion/followers","following_url":"https://api.github.com/users/illicitonion/following{/other_user}","gists_url":"https://api.github.com/users/illicitonion/gists{/gist_id}","starred_url":"https://api.github.com/users/illicitonion/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/illicitonion/subscriptions","organizations_url":"https://api.github.com/users/illicitonion/orgs","repos_url":"https://api.github.com/users/illicitonion/repos","events_url":"https://api.github.com/users/illicitonion/events{/privacy}","received_events_url":"https://api.github.com/users/illicitonion/received_events","type":"User","site_admin":false},"body":"I don't follow this... Is this required? Or an optimisation?","created_at":"2018-07-25T14:01:12Z","updated_at":"2018-07-25T14:32:43Z","html_url":"https://github.com/pantsbuild/pants/pull/6170#discussion_r205120219","pull_request_url":"https://api.github.com/repos/pantsbuild/pants/pulls/6170","author_association":"CONTRIBUTOR","_links":{"self":{"href":"https://api.github.com/repos/pantsbuild/pants/pulls/comments/205120219"},"html":{"href":"https://github.com/pantsbuild/pants/pull/6170#discussion_r205120219"},"pull_request":{"href":"https://api.github.com/repos/pantsbuild/pants/pulls/6170"}}},"pull_request":{"url":"https://api.github.com/repos/pantsbuild/pants/pulls/6170","id":202103687,"node_id":"MDExOlB1bGxSZXF1ZXN0MjAyMTAzNjg3","html_url":"https://github.com/pantsbuild/pants/pull/6170","diff_url":"https://github.com/pantsbuild/pants/pull/6170.diff","patch_url":"https://github.com/pantsbuild/pants/pull/6170.patch","issue_url":"https://api.github.com/repos/pantsbuild/pants/issues/6170","number":6170,"state":"open","locked":false,"title":"Merge subjects and variants into Params, and remove Noop","user":{"login":"stuhood","id":46740,"node_id":"MDQ6VXNlcjQ2NzQw","avatar_url":"https://avatars1.githubusercontent.com/u/46740?v=4","gravatar_id":"","url":"https://api.github.com/users/stuhood","html_url":"https://github.com/stuhood","followers_url":"https://api.github.com/users/stuhood/followers","following_url":"https://api.github.com/users/stuhood/following{/other_user}","gists_url":"https://api.github.com/users/stuhood/gists{/gist_id}","starred_url":"https://api.github.com/users/stuhood/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/stuhood/subscriptions","organizations_url":"https://api.github.com/users/stuhood/orgs","repos_url":"https://api.github.com/users/stuhood/repos","events_url":"https://api.github.com/users/stuhood/events{/privacy}","received_events_url":"https://api.github.com/users/stuhood/received_events","type":"User","site_admin":false},"body":"### Problem\r\n\r\nAs described in #5788: `@rules` need a way to rely on values that are provided at request time, but which do not necessarily participate in the signatures of all `@rules` between the root and the consuming `@rule`. This is related to the existing concept of \"variants\", but requires an implementation that does not introduce variants into `Node` identities in subgraphs where they are not required.\r\n\r\nAdditionally, as described a while back in #4304, it should be possible to generate concrete subgraphs by removing ambiguity from the `RuleGraph`... but ambiguity is currently a \"feature\" required for composability of `@rule`s that do not know about one another.\r\n\r\n### Solution\r\n\r\nThis change merges `Variants` and \"subjects\" into `Params`, and statically determines which `Params` are required in each subgraph. In order to handle cases where multiple providers of an `@rule` type dependency are available with different required inputs `Params`, the change \"monomorphizes\" (duplicates) `RuleGraph` entries per used parameter set. This allows us to remove runtime `Noop`s, because every `RuleGraph` entry (and thus `Node`) has exactly one `@rule` provider for each of its declared dependencies.\r\n\r\nWhile pants' own installed `@rule`s do not contain any ambiguity, this change adds `AggregationRule` to solve #2526 and unblock composing `@rules` that provide a single type in an uncoordinated fashion.\r\n\r\n### Result\r\n\r\nLays groundwork for #4020 and #5788. Fixes #2526 by adding `AggregationRule`. Fixes #4304 by monomorphizing `RuleGraph` entries and removing `Noop`. Fixes #4027 by... deleting that code.\r\n\r\nThis change does not yet expose any sort of UX for providing more than one `Param` in a `Get` or root request, but it was already way too large, so I think that should wait for a followup.","created_at":"2018-07-17T23:47:06Z","updated_at":"2018-07-25T14:32:43Z","closed_at":null,"merged_at":null,"merge_commit_sha":"4db12475aebe9db6b744545c8d4986254eac2a79","assignee":null,"assignees":[],"requested_reviewers":[{"login":"baroquebobcat","id":7378,"node_id":"MDQ6VXNlcjczNzg=","avatar_url":"https://avatars0.githubusercontent.com/u/7378?v=4","gravatar_id":"","url":"https://api.github.com/users/baroquebobcat","html_url":"https://github.com/baroquebobcat","followers_url":"https://api.github.com/users/baroquebobcat/followers","following_url":"https://api.github.com/users/baroquebobcat/following{/other_user}","gists_url":"https://api.github.com/users/baroquebobcat/gists{/gist_id}","starred_url":"https://api.github.com/users/baroquebobcat/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/baroquebobcat/subscriptions","organizations_url":"https://api.github.com/users/baroquebobcat/orgs","repos_url":"https://api.github.com/users/baroquebobcat/repos","events_url":"https://api.github.com/users/baroquebobcat/events{/privacy}","received_events_url":"https://api.github.com/users/baroquebobcat/received_events","type":"User","site_admin":false},{"login":"ity","id":1278541,"node_id":"MDQ6VXNlcjEyNzg1NDE=","avatar_url":"https://avatars3.githubusercontent.com/u/1278541?v=4","gravatar_id":"","url":"https://api.github.com/users/ity","html_url":"https://github.com/ity","followers_url":"https://api.github.com/users/ity/followers","following_url":"https://api.github.com/users/ity/following{/other_user}","gists_url":"https://api.github.com/users/ity/gists{/gist_id}","starred_url":"https://api.github.com/users/ity/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ity/subscriptions","organizations_url":"https://api.github.com/users/ity/orgs","repos_url":"https://api.github.com/users/ity/repos","events_url":"https://api.github.com/users/ity/events{/privacy}","received_events_url":"https://api.github.com/users/ity/received_events","type":"User","site_admin":false},{"login":"cosmicexplorer","id":1305167,"node_id":"MDQ6VXNlcjEzMDUxNjc=","avatar_url":"https://avatars1.githubusercontent.com/u/1305167?v=4","gravatar_id":"","url":"https://api.github.com/users/cosmicexplorer","html_url":"https://github.com/cosmicexplorer","followers_url":"https://api.github.com/users/cosmicexplorer/followers","following_url":"https://api.github.com/users/cosmicexplorer/following{/other_user}","gists_url":"https://api.github.com/users/cosmicexplorer/gists{/gist_id}","starred_url":"https://api.github.com/users/cosmicexplorer/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cosmicexplorer/subscriptions","organizations_url":"https://api.github.com/users/cosmicexplorer/orgs","repos_url":"https://api.github.com/users/cosmicexplorer/repos","events_url":"https://api.github.com/users/cosmicexplorer/events{/privacy}","received_events_url":"https://api.github.com/users/cosmicexplorer/received_events","type":"User","site_admin":false}],"requested_teams":[],"labels":[],"milestone":null,"commits_url":"https://api.github.com/repos/pantsbuild/pants/pulls/6170/commits","review_comments_url":"https://api.github.com/repos/pantsbuild/pants/pulls/6170/comments","review_comment_url":"https://api.github.com/repos/pantsbuild/pants/pulls/comments{/number}","comments_url":"https://api.github.com/repos/pantsbuild/pants/issues/6170/comments","statuses_url":"https://api.github.com/repos/pantsbuild/pants/statuses/438e607a8519f9b3d8a31195fe03050e070af0a1","head":{"label":"twitter:stuhood/subsingliant","ref":"stuhood/subsingliant","sha":"438e607a8519f9b3d8a31195fe03050e070af0a1","user":{"login":"twitter","id":50278,"node_id":"MDEyOk9yZ2FuaXphdGlvbjUwMjc4","avatar_url":"https://avatars1.githubusercontent.com/u/50278?v=4","gravatar_id":"","url":"https://api.github.com/users/twitter","html_url":"https://github.com/twitter","followers_url":"https://api.github.com/users/twitter/followers","following_url":"https://api.github.com/users/twitter/following{/other_user}","gists_url":"https://api.github.com/users/twitter/gists{/gist_id}","starred_url":"https://api.github.com/users/twitter/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/twitter/subscriptions","organizations_url":"https://api.github.com/users/twitter/orgs","repos_url":"https://api.github.com/users/twitter/repos","events_url":"https://api.github.com/users/twitter/events{/privacy}","received_events_url":"https://api.github.com/users/twitter/received_events","type":"Organization","site_admin":false},"repo":{"id":29317658,"node_id":"MDEwOlJlcG9zaXRvcnkyOTMxNzY1OA==","name":"pants","full_name":"twitter/pants","owner":{"login":"twitter","id":50278,"node_id":"MDEyOk9yZ2FuaXphdGlvbjUwMjc4","avatar_url":"https://avatars1.githubusercontent.com/u/50278?v=4","gravatar_id":"","url":"https://api.github.com/users/twitter","html_url":"https://github.com/twitter","followers_url":"https://api.github.com/users/twitter/followers","following_url":"https://api.github.com/users/twitter/following{/other_user}","gists_url":"https://api.github.com/users/twitter/gists{/gist_id}","starred_url":"https://api.github.com/users/twitter/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/twitter/subscriptions","organizations_url":"https://api.github.com/users/twitter/orgs","repos_url":"https://api.github.com/users/twitter/repos","events_url":"https://api.github.com/users/twitter/events{/privacy}","received_events_url":"https://api.github.com/users/twitter/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/twitter/pants","description":"Pants Build System","fork":true,"url":"https://api.github.com/repos/twitter/pants","forks_url":"https://api.github.com/repos/twitter/pants/forks","keys_url":"https://api.github.com/repos/twitter/pants/keys{/key_id}","collaborators_url":"https://api.github.com/repos/twitter/pants/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/twitter/pants/teams","hooks_url":"https://api.github.com/repos/twitter/pants/hooks","issue_events_url":"https://api.github.com/repos/twitter/pants/issues/events{/number}","events_url":"https://api.github.com/repos/twitter/pants/events","assignees_url":"https://api.github.com/repos/twitter/pants/assignees{/user}","branches_url":"https://api.github.com/repos/twitter/pants/branches{/branch}","tags_url":"https://api.github.com/repos/twitter/pants/tags","blobs_url":"https://api.github.com/repos/twitter/pants/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/twitter/pants/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/twitter/pants/git/refs{/sha}","trees_url":"https://api.github.com/repos/twitter/pants/git/trees{/sha}","statuses_url":"https://api.github.com/repos/twitter/pants/statuses/{sha}","languages_url":"https://api.github.com/repos/twitter/pants/languages","stargazers_url":"https://api.github.com/repos/twitter/pants/stargazers","contributors_url":"https://api.github.com/repos/twitter/pants/contributors","subscribers_url":"https://api.github.com/repos/twitter/pants/subscribers","subscription_url":"https://api.github.com/repos/twitter/pants/subscription","commits_url":"https://api.github.com/repos/twitter/pants/commits{/sha}","git_commits_url":"https://api.github.com/repos/twitter/pants/git/commits{/sha}","comments_url":"https://api.github.com/repos/twitter/pants/comments{/number}","issue_comment_url":"https://api.github.com/repos/twitter/pants/issues/comments{/number}","contents_url":"https://api.github.com/repos/twitter/pants/contents/{+path}","compare_url":"https://api.github.com/repos/twitter/pants/compare/{base}...{head}","merges_url":"https://api.github.com/repos/twitter/pants/merges","archive_url":"https://api.github.com/repos/twitter/pants/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/twitter/pants/downloads","issues_url":"https://api.github.com/repos/twitter/pants/issues{/number}","pulls_url":"https://api.github.com/repos/twitter/pants/pulls{/number}","milestones_url":"https://api.github.com/repos/twitter/pants/milestones{/number}","notifications_url":"https://api.github.com/repos/twitter/pants/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/twitter/pants/labels{/name}","releases_url":"https://api.github.com/repos/twitter/pants/releases{/id}","deployments_url":"https://api.github.com/repos/twitter/pants/deployments","created_at":"2015-01-15T20:51:58Z","updated_at":"2018-07-25T01:58:13Z","pushed_at":"2018-07-25T04:49:11Z","git_url":"git://github.com/twitter/pants.git","ssh_url":"git@github.com:twitter/pants.git","clone_url":"https://github.com/twitter/pants.git","svn_url":"https://github.com/twitter/pants","homepage":"http://pantsbuild.github.io/","size":119607,"stargazers_count":62,"watchers_count":62,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":20,"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","node_id":"MDc6TGljZW5zZTI="},"forks":20,"open_issues":0,"watchers":62,"default_branch":"master"}},"base":{"label":"pantsbuild:master","ref":"master","sha":"e5a5e5ed40ae7312d523207c4ddd888909c6ff8c","user":{"login":"pantsbuild","id":3065172,"node_id":"MDEyOk9yZ2FuaXphdGlvbjMwNjUxNzI=","avatar_url":"https://avatars0.githubusercontent.com/u/3065172?v=4","gravatar_id":"","url":"https://api.github.com/users/pantsbuild","html_url":"https://github.com/pantsbuild","followers_url":"https://api.github.com/users/pantsbuild/followers","following_url":"https://api.github.com/users/pantsbuild/following{/other_user}","gists_url":"https://api.github.com/users/pantsbuild/gists{/gist_id}","starred_url":"https://api.github.com/users/pantsbuild/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pantsbuild/subscriptions","organizations_url":"https://api.github.com/users/pantsbuild/orgs","repos_url":"https://api.github.com/users/pantsbuild/repos","events_url":"https://api.github.com/users/pantsbuild/events{/privacy}","received_events_url":"https://api.github.com/users/pantsbuild/received_events","type":"Organization","site_admin":false},"repo":{"id":7209075,"node_id":"MDEwOlJlcG9zaXRvcnk3MjA5MDc1","name":"pants","full_name":"pantsbuild/pants","owner":{"login":"pantsbuild","id":3065172,"node_id":"MDEyOk9yZ2FuaXphdGlvbjMwNjUxNzI=","avatar_url":"https://avatars0.githubusercontent.com/u/3065172?v=4","gravatar_id":"","url":"https://api.github.com/users/pantsbuild","html_url":"https://github.com/pantsbuild","followers_url":"https://api.github.com/users/pantsbuild/followers","following_url":"https://api.github.com/users/pantsbuild/following{/other_user}","gists_url":"https://api.github.com/users/pantsbuild/gists{/gist_id}","starred_url":"https://api.github.com/users/pantsbuild/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pantsbuild/subscriptions","organizations_url":"https://api.github.com/users/pantsbuild/orgs","repos_url":"https://api.github.com/users/pantsbuild/repos","events_url":"https://api.github.com/users/pantsbuild/events{/privacy}","received_events_url":"https://api.github.com/users/pantsbuild/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/pantsbuild/pants","description":"The Pants Build System","fork":false,"url":"https://api.github.com/repos/pantsbuild/pants","forks_url":"https://api.github.com/repos/pantsbuild/pants/forks","keys_url":"https://api.github.com/repos/pantsbuild/pants/keys{/key_id}","collaborators_url":"https://api.github.com/repos/pantsbuild/pants/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/pantsbuild/pants/teams","hooks_url":"https://api.github.com/repos/pantsbuild/pants/hooks","issue_events_url":"https://api.github.com/repos/pantsbuild/pants/issues/events{/number}","events_url":"https://api.github.com/repos/pantsbuild/pants/events","assignees_url":"https://api.github.com/repos/pantsbuild/pants/assignees{/user}","branches_url":"https://api.github.com/repos/pantsbuild/pants/branches{/branch}","tags_url":"https://api.github.com/repos/pantsbuild/pants/tags","blobs_url":"https://api.github.com/repos/pantsbuild/pants/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/pantsbuild/pants/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/pantsbuild/pants/git/refs{/sha}","trees_url":"https://api.github.com/repos/pantsbuild/pants/git/trees{/sha}","statuses_url":"https://api.github.com/repos/pantsbuild/pants/statuses/{sha}","languages_url":"https://api.github.com/repos/pantsbuild/pants/languages","stargazers_url":"https://api.github.com/repos/pantsbuild/pants/stargazers","contributors_url":"https://api.github.com/repos/pantsbuild/pants/contributors","subscribers_url":"https://api.github.com/repos/pantsbuild/pants/subscribers","subscription_url":"https://api.github.com/repos/pantsbuild/pants/subscription","commits_url":"https://api.github.com/repos/pantsbuild/pants/commits{/sha}","git_commits_url":"https://api.github.com/repos/pantsbuild/pants/git/commits{/sha}","comments_url":"https://api.github.com/repos/pantsbuild/pants/comments{/number}","issue_comment_url":"https://api.github.com/repos/pantsbuild/pants/issues/comments{/number}","contents_url":"https://api.github.com/repos/pantsbuild/pants/contents/{+path}","compare_url":"https://api.github.com/repos/pantsbuild/pants/compare/{base}...{head}","merges_url":"https://api.github.com/repos/pantsbuild/pants/merges","archive_url":"https://api.github.com/repos/pantsbuild/pants/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/pantsbuild/pants/downloads","issues_url":"https://api.github.com/repos/pantsbuild/pants/issues{/number}","pulls_url":"https://api.github.com/repos/pantsbuild/pants/pulls{/number}","milestones_url":"https://api.github.com/repos/pantsbuild/pants/milestones{/number}","notifications_url":"https://api.github.com/repos/pantsbuild/pants/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/pantsbuild/pants/labels{/name}","releases_url":"https://api.github.com/repos/pantsbuild/pants/releases{/id}","deployments_url":"https://api.github.com/repos/pantsbuild/pants/deployments","created_at":"2012-12-17T17:39:04Z","updated_at":"2018-07-25T03:06:23Z","pushed_at":"2018-07-25T13:47:01Z","git_url":"git://github.com/pantsbuild/pants.git","ssh_url":"git@github.com:pantsbuild/pants.git","clone_url":"https://github.com/pantsbuild/pants.git","svn_url":"https://github.com/pantsbuild/pants","homepage":"http://pantsbuild.org/","size":86256,"stargazers_count":989,"watchers_count":989,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":286,"mirror_url":null,"archived":false,"open_issues_count":664,"license":{"key":"apache-2.0","name":"Apache License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0","node_id":"MDc6TGljZW5zZTI="},"forks":286,"open_issues":664,"watchers":989,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/pantsbuild/pants/pulls/6170"},"html":{"href":"https://github.com/pantsbuild/pants/pull/6170"},"issue":{"href":"https://api.github.com/repos/pantsbuild/pants/issues/6170"},"comments":{"href":"https://api.github.com/repos/pantsbuild/pants/issues/6170/comments"},"review_comments":{"href":"https://api.github.com/repos/pantsbuild/pants/pulls/6170/comments"},"review_comment":{"href":"https://api.github.com/repos/pantsbuild/pants/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/pantsbuild/pants/pulls/6170/commits"},"statuses":{"href":"https://api.github.com/repos/pantsbuild/pants/statuses/438e607a8519f9b3d8a31195fe03050e070af0a1"}},"author_association":"MEMBER"}}
{"actor":{"display_login":"illicitonion"}}