diff --git a/cmd/chisel/cmd_conflicts.go b/cmd/chisel/cmd_conflicts.go
new file mode 100644
index 000000000..80e5a17db
--- /dev/null
+++ b/cmd/chisel/cmd_conflicts.go
@@ -0,0 +1,156 @@
+package main
+
+import (
+ "fmt"
+ "sort"
+ "strings"
+
+ "github.com/jessevdk/go-flags"
+
+ "github.com/canonical/chisel/internal/setup"
+ "github.com/canonical/chisel/internal/strdist"
+)
+
+var shortConflictHelp = "Count number of conlicting implicit parent dirs"
+var longConflictHelp = `
+Count number of conlicting implicit parent dirs
+`
+
+var conflictDescs = map[string]string{
+ "release": "Chisel release name or directory (e.g. ubuntu-22.04)",
+ "details": "Show conflict details",
+}
+
+type cmdConflict struct {
+ Release string `long:"release" value-name:"
"`
+ Details bool `long:"details"`
+}
+
+func init() {
+ addCommand(
+ "conflicts", shortConflictHelp, longConflictHelp,
+ func() flags.Commander { return &cmdConflict{} },
+ conflictDescs, nil,
+ )
+}
+
+func (cmd *cmdConflict) Execute(args []string) error {
+ if len(args) > 0 {
+ return ErrExtraArgs
+ }
+
+ release, err := obtainRelease(cmd.Release)
+ if err != nil {
+ return err
+ }
+
+ conflicts, err := getConflicts(release)
+ if err != nil {
+ return err
+ }
+
+ w := tabWriter()
+ defer w.Flush()
+ fmt.Fprintf(Stdout, "Total conflicts: %d\n", len(conflicts))
+ if cmd.Details {
+ keys := make([]string, 0, len(conflicts))
+ for p := range conflicts {
+ keys = append(keys, p)
+ }
+ sort.Strings(keys)
+ for _, p := range keys {
+ c := conflicts[p]
+ fmt.Fprintf(w, "%s\t%s\t%s\n", p, c.path, c.reason)
+ }
+ }
+ return nil
+}
+
+type conflictInfo struct {
+ path string
+ reason string
+}
+
+// Get all conflicts.
+//
+// Assumptions:
+// - Same paths across slices/packages are considered the same path.
+// - Paths conflict if they share at least one ancestor.
+func getConflicts(r *setup.Release) (map[string]*conflictInfo, error) {
+ if r == nil {
+ return nil, nil
+ }
+
+ var paths []string
+ for _, pkg := range r.Packages {
+ for _, slice := range pkg.Slices {
+ for p := range slice.Contents {
+ paths = append(paths, p)
+ }
+ }
+ }
+
+ c := make(map[string]*conflictInfo)
+ for i, p := range paths {
+ for _, q := range paths[:i] {
+ prefix := hasConflict(p, q)
+ if prefix != "" {
+ c[p] = &conflictInfo{
+ path: q,
+ reason: prefix,
+ }
+ c[q] = &conflictInfo{
+ path: p,
+ reason: prefix,
+ }
+ }
+ }
+ }
+ return c, nil
+}
+
+// Returns the conflicting prefix.
+func hasConflict(p, q string) string {
+ ps := strings.Split(p, "/")[1:]
+ qs := strings.Split(q, "/")[1:]
+
+ if len(ps) == 0 || len(qs) == 0 {
+ return ""
+ }
+
+ if len(ps) == 1 || len(qs) == 1 {
+ var wild bool
+ a := ps[0]
+ b := qs[0]
+ if i := strings.Index(a, "**"); i != -1 {
+ a = a[:i] + "**"
+ wild = true
+ }
+ if i := strings.Index(b, "**"); i != -1 {
+ b = b[:i] + "**"
+ wild = true
+ }
+ if !wild {
+ // One of them must have **.
+ return ""
+ }
+ if a == b || strdist.GlobPath(a, b) {
+ return "/" + a
+ }
+ return ""
+ }
+
+ ps = ps[:len(ps)-1]
+ qs = qs[:len(qs)-1]
+
+ if ps[0] == qs[0] {
+ // First directory matches.
+ return "/" + ps[0]
+ }
+
+ if strdist.GlobPath(ps[0], qs[0]) {
+ return "/" + ps[0]
+ }
+
+ return ""
+}
diff --git a/cmd/chisel/cmd_conflicts_test.go b/cmd/chisel/cmd_conflicts_test.go
new file mode 100644
index 000000000..85858eae7
--- /dev/null
+++ b/cmd/chisel/cmd_conflicts_test.go
@@ -0,0 +1,69 @@
+package main_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ chisel "github.com/canonical/chisel/cmd/chisel"
+)
+
+type conflictTest struct {
+ a, b string
+ cnfl string
+}
+
+var conflictTests = []conflictTest{{
+ a: "/bin/foo",
+ b: "/bin/bar",
+ cnfl: "/bin",
+}, {
+ a: "/dir/*/foo",
+ b: "/dir/bar/bar",
+ cnfl: "/dir",
+}, {
+ a: "/*/foo",
+ b: "/dir/bar",
+ cnfl: "/*",
+}, {
+ a: "/*a/foo",
+ b: "/dir/bar",
+ cnfl: "",
+}, {
+ a: "/*r/foo",
+ b: "/dir/bar",
+ cnfl: "/*r",
+}, {
+ a: "/d?r/foo",
+ b: "/dir/bar",
+ cnfl: "/d?r",
+}, {
+ a: "/dir?/foo",
+ b: "/dir/bar",
+ cnfl: "",
+}, {
+ a: "/dir*/foo",
+ b: "/d*/bar",
+ cnfl: "/dir*",
+}, {
+ a: "/**",
+ b: "/dir/bar",
+ cnfl: "/**",
+}, {
+ a: "/d**r",
+ b: "/dir/bar",
+ cnfl: "/d**",
+}, {
+ a: "/d**r",
+ b: "/foo/bar",
+ cnfl: "",
+}, {
+ a: "/dir/*/foo",
+ b: "/xyz/**",
+ cnfl: "",
+}}
+
+func (s *ChiselSuite) TestConflicts(c *C) {
+ for _, test := range conflictTests {
+ s := chisel.HasConflict(test.a, test.b)
+ c.Assert(s, Equals, test.cnfl)
+ }
+}
diff --git a/cmd/chisel/cmd_help.go b/cmd/chisel/cmd_help.go
index d164d99f6..44fbfccb8 100644
--- a/cmd/chisel/cmd_help.go
+++ b/cmd/chisel/cmd_help.go
@@ -155,7 +155,7 @@ type helpCategory struct {
var helpCategories = []helpCategory{{
Label: "Basic",
Description: "general operations",
- Commands: []string{"find", "info", "help", "version"},
+ Commands: []string{"find", "info", "conflicts", "help", "version"},
}, {
Label: "Action",
Description: "make things happen",
diff --git a/cmd/chisel/export_test.go b/cmd/chisel/export_test.go
index a99a7831e..ac255efd3 100644
--- a/cmd/chisel/export_test.go
+++ b/cmd/chisel/export_test.go
@@ -19,3 +19,5 @@ func FakeIsStdinTTY(t bool) (restore func()) {
}
var FindSlices = findSlices
+
+var HasConflict = hasConflict