Allow `+` in repo names (#2908)

Bazel 8 uses `+` as the separator for segments of canonical repo names,
whereas Bazel 7 uses `~` or `+` depending on the value of
`--incompatible_use_plus_in_repo_names`.

---------

Co-authored-by: Daniel Wagner-Hall <dawagner@gmail.com>
diff --git a/crate_universe/src/utils/starlark/label.rs b/crate_universe/src/utils/starlark/label.rs
index 30e1d23..58abbce 100644
--- a/crate_universe/src/utils/starlark/label.rs
+++ b/crate_universe/src/utils/starlark/label.rs
@@ -66,7 +66,8 @@
     fn from_str(s: &str) -> Result<Self, Self::Err> {
         static RE: OnceCell<Regex> = OnceCell::new();
         let re = RE.get_or_try_init(|| {
-            Regex::new(r"^(@@?[\w\d\-_\.~]*)?(//)?([\w\d\-_\./+]+)?(:([\+\w\d\-_\./]+))?$")
+            // TODO: Disallow `~` in repository names once support for Bazel 7.2 is dropped.
+            Regex::new(r"^(@@?[\w\d\-_\.+~]*)?(//)?([\w\d\-_\./+]+)?(:([\+\w\d\-_\./]+))?$")
         });
 
         let cap = re?
diff --git a/util/label/label.rs b/util/label/label.rs
index d1e00a7..7f832e1 100644
--- a/util/label/label.rs
+++ b/util/label/label.rs
@@ -14,7 +14,7 @@
 
 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
 pub enum Repository<'s> {
-    /// A `@@` prefixed name that is unique within a workspace. E.g. `@@rules_rust~0.1.2~toolchains~local_rustc`
+    /// A `@@` prefixed name that is unique within a workspace. E.g. `@@rules_rust+0.1.2+toolchains~local_rustc`
     Canonical(&'s str), // stringifies to `@@self.0` where `self.0` may be empty
     /// A `@` (single) prefixed name. E.g. `@rules_rust`.
     Apparent(&'s str),
@@ -187,12 +187,20 @@
         }
         if !repository_name
             .chars()
-            .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.' || c == '~')
+            // TODO: Disallow `~` in repository names once support for Bazel 7.2 is dropped.
+            .all(|c| {
+                c.is_ascii_alphanumeric()
+                    || c == '-'
+                    || c == '_'
+                    || c == '.'
+                    || c == '~'
+                    || c == '+'
+            })
         {
             return Err(LabelError(err(
                 label,
                 "workspace names \
-                may contain only A-Z, a-z, 0-9, '-', '_', '.', and '~'.",
+                may contain only A-Z, a-z, 0-9, '-', '_', '.', '+', and '~'.",
             )));
         }
     }
@@ -334,6 +342,7 @@
     #[test]
     fn test_repository_name_parsing() -> Result<()> {
         assert_eq!(analyze("@repo//:foo")?.repo_name(), Some("repo"));
+        assert_eq!(analyze("@repo+name//:foo")?.repo_name(), Some("repo+name"));
         assert_eq!(analyze("@@repo//:foo")?.repo_name(), Some("repo"));
         assert_eq!(analyze("@//:foo")?.repo_name(), Some(""));
         assert_eq!(analyze("//:foo")?.repo_name(), None);
@@ -341,6 +350,10 @@
 
         assert_eq!(analyze("@repo//foo/bar")?.repo_name(), Some("repo"));
         assert_eq!(analyze("@@repo//foo/bar")?.repo_name(), Some("repo"));
+        assert_eq!(
+            analyze("@@repo+name//foo/bar")?.repo_name(),
+            Some("repo+name")
+        );
         assert_eq!(analyze("@//foo/bar")?.repo_name(), Some(""));
         assert_eq!(analyze("//foo/bar")?.repo_name(), None);
         assert_eq!(
@@ -378,7 +391,7 @@
         assert_eq!(
             analyze("@foo:bar"),
             Err(LabelError(
-                "@foo:bar must be a legal label; workspace names may contain only A-Z, a-z, 0-9, '-', '_', '.', and '~'.".to_string()
+                "@foo:bar must be a legal label; workspace names may contain only A-Z, a-z, 0-9, '-', '_', '.', '+', and '~'.".to_string()
             ))
         );
 
@@ -398,7 +411,7 @@
             analyze("@foo#//:baz"),
             Err(LabelError(
                 "@foo#//:baz must be a legal label; workspace names \
-            may contain only A-Z, a-z, 0-9, '-', '_', '.', and '~'."
+            may contain only A-Z, a-z, 0-9, '-', '_', '.', '+', and '~'."
                     .to_string()
             ))
         );