Create inheritance graph

release-config now creates `inhertance_graph-${TARGET_PRODUCT}.dot`
showing the inheritance graph for all release configs present for
${TARGET_PRODUCT}.

Bug: 328495189
Test: manual, TH
Change-Id: I79242eaa848e0374b33f376e44d8938e9d398e21
diff --git a/cmd/release_config/release_config/main.go b/cmd/release_config/release_config/main.go
index 762f561..24deacc 100644
--- a/cmd/release_config/release_config/main.go
+++ b/cmd/release_config/release_config/main.go
@@ -31,7 +31,7 @@
 	var outputDir string
 	var err error
 	var configs *rc_lib.ReleaseConfigs
-	var json, pb, textproto bool
+	var json, pb, textproto, inheritance bool
 	var product string
 	var allMake bool
 	var useBuildVar bool
@@ -52,6 +52,7 @@
 	flag.BoolVar(&json, "json", true, "write artifacts as json")
 	flag.BoolVar(&pb, "pb", true, "write artifacts as binary protobuf")
 	flag.BoolVar(&allMake, "all_make", false, "write makefiles for all release configs")
+	flag.BoolVar(&inheritance, "inheritance", true, "write inheritance graph")
 	flag.BoolVar(&useBuildVar, "use_get_build_var", false, "use get_build_var PRODUCT_RELEASE_CONFIG_MAPS")
 	flag.BoolVar(&guard, "guard", true, "whether to guard with RELEASE_BUILD_FLAGS_IN_PROTOBUF")
 
@@ -102,6 +103,13 @@
 			}
 		}
 	}
+	if inheritance {
+		inheritPath := filepath.Join(outputDir, fmt.Sprintf("inheritance_graph-%s.dot", product))
+		err = configs.WriteInheritanceGraph(inheritPath)
+		if err != nil {
+			panic(err)
+		}
+	}
 	if json {
 		err = configs.WriteArtifact(outputDir, product, "json")
 		if err != nil {
diff --git a/cmd/release_config/release_config_lib/release_configs.go b/cmd/release_config/release_config_lib/release_configs.go
index c62a78e..bad9144 100644
--- a/cmd/release_config/release_config_lib/release_configs.go
+++ b/cmd/release_config/release_config_lib/release_configs.go
@@ -78,6 +78,54 @@
 	configDirIndexes ReleaseConfigDirMap
 }
 
+func (configs *ReleaseConfigs) WriteInheritanceGraph(outFile string) error {
+	data := []string{
+		"digraph {",
+		"graph [ ratio=.5 ]",
+		"node [ shape=box style=filled fillcolor=white colorscheme=svg fontcolor=black ]",
+	}
+	usedAliases := make(map[string]bool)
+	for _, config := range configs.GetSortedReleaseConfigs() {
+		var fillColor string
+		inherits := []string{}
+		for _, inherit := range config.InheritNames {
+			if inherit == "root" {
+				// Only show "root" if we have no other inheritance.
+				if len(config.InheritNames) > 1 {
+					continue
+				}
+			}
+			data = append(data, fmt.Sprintf(`"%s" -> "%s"`, config.Name, inherit))
+			inherits = append(inherits, inherit)
+			// If inheriting an alias, add a link from the alias to that release config.
+			if name, found := configs.Aliases[inherit]; found {
+				if !usedAliases[inherit] {
+					usedAliases[inherit] = true
+					data = append(data, fmt.Sprintf(`"%s" -> "%s"`, inherit, *name))
+					data = append(data,
+						fmt.Sprintf(`"%s" [ label="%s\ncurrently: %s" shape=oval ]`,
+							inherit, inherit, *name))
+				}
+			}
+		}
+		label := config.Name
+		if len(inherits) > 0 {
+			label += "\\ninherits: " + strings.Join(inherits, " ")
+		}
+		if len(config.OtherNames) > 0 {
+			label += "\\nother names: " + strings.Join(config.OtherNames, " ")
+		}
+		// The active release config has a light blue fill.
+		if config.Name == *configs.Artifact.ReleaseConfig.Name {
+			fillColor = `fillcolor="#d2e3fc" `
+		}
+		data = append(data,
+			fmt.Sprintf(`"%s" [ label="%s" %s]`, config.Name, label, fillColor))
+	}
+	data = append(data, "}")
+	return os.WriteFile(outFile, []byte(strings.Join(data, "\n")), 0644)
+}
+
 // Write the "all_release_configs" artifact.
 //
 // The file will be in "{outDir}/all_release_configs-{product}.{format}"
@@ -256,7 +304,17 @@
 		}
 		config := configs.ReleaseConfigs[name]
 		config.FilesUsedMap[path] = true
-		config.InheritNames = append(config.InheritNames, releaseConfigContribution.proto.Inherits...)
+		inheritNames := make(map[string]bool)
+		for _, inh := range config.InheritNames {
+			inheritNames[inh] = true
+		}
+		// If this contribution says to inherit something we already inherited, we do not want the duplicate.
+		for _, cInh := range releaseConfigContribution.proto.Inherits {
+			if !inheritNames[cInh] {
+				config.InheritNames = append(config.InheritNames, cInh)
+				inheritNames[cInh] = true
+			}
+		}
 
 		// Only walk flag_values/{RELEASE} for defined releases.
 		err2 := WalkTextprotoFiles(dir, filepath.Join("flag_values", name), func(path string, d fs.DirEntry, err error) error {