Escape tag-pattern components when constructing tag matching RegExp (#4878)
diff --git a/lib/src/source/git.dart b/lib/src/source/git.dart
index 9dd19fe..687ba7a 100644
--- a/lib/src/source/git.dart
+++ b/lib/src/source/git.dart
@@ -808,7 +808,7 @@
         version = Version.parse(match[1]!);
       } on FormatException catch (e) {
         throw StateError(
-          'Matched part ${Version.parse(match[1]!)} did not match version $e.',
+          'Matched part "${match[1]}" did not match version: $e.',
         );
       }
       result.add((version: version, commitId: parts[1]));
@@ -1206,8 +1206,8 @@
 /// [tagPatternVersionMarker].
 RegExp compileTagPattern(String tagPattern) {
   final parts = tagPattern.split(tagPatternVersionMarker);
-  final before = parts[0];
-  final after = parts[1];
+  final before = RegExp.escape(parts[0]);
+  final after = RegExp.escape(parts[1]);
 
   return RegExp(
     r'^'
diff --git a/test/get/git/tag_pattern_test.dart b/test/get/git/tag_pattern_test.dart
index a34e72a..64bd93b 100644
--- a/test/get/git/tag_pattern_test.dart
+++ b/test/get/git/tag_pattern_test.dart
@@ -9,6 +9,7 @@
 
 import 'package:pub/src/exit_codes.dart';
 import 'package:pub/src/path.dart';
+import 'package:pub/src/source/git.dart';
 import 'package:test/test.dart';
 import 'package:yaml/yaml.dart';
 
@@ -494,5 +495,82 @@
         });
       },
     );
+
+    test(
+      '(${tagType.name}) tag_pattern matches special regex characters verbatim',
+      () async {
+        ensureGit();
+        await d.git('foo.git', [
+          d.libPubspec('foo', '1.0.0', sdk: '^3.9.0'),
+        ]).create();
+        await d.git('foo.git', []).tag('fooxbar-1.0.0', tagType: tagType);
+        await d.git('foo.git', [
+          d.libPubspec('foo', '2.0.0', sdk: '^3.9.0'),
+        ]).commit();
+        await d.git('foo.git', []).tag('foo.bar-2.0.0', tagType: tagType);
+
+        await d
+            .appDir(
+              dependencies: {
+                'foo': {
+                  'git': {
+                    'url': p.join(d.sandbox, 'foo.git'),
+                    'tag_pattern': 'foo.bar-{{version}}',
+                  },
+                  'version': '^2.0.0',
+                },
+              },
+              pubspec: {
+                'environment': {'sdk': '^3.9.0'},
+              },
+            )
+            .create();
+
+        await pubGet(
+          output: contains('+ foo 2.0.0'),
+          environment: {'_PUB_TEST_SDK_VERSION': '3.9.0'},
+        );
+      },
+    );
+
+    test('(${tagType.name}) tag_pattern with parentheses does not '
+        'break capture groups', () async {
+      ensureGit();
+      await d.git('foo.git', [
+        d.libPubspec('foo', '1.0.0', sdk: '^3.9.0'),
+      ]).create();
+      await d.git('foo.git', []).tag('(v)-1.0.0', tagType: tagType);
+
+      await d
+          .appDir(
+            dependencies: {
+              'foo': {
+                'git': {
+                  'url': p.join(d.sandbox, 'foo.git'),
+                  'tag_pattern': '(v)-{{version}}',
+                },
+                'version': '^1.0.0',
+              },
+            },
+            pubspec: {
+              'environment': {'sdk': '^3.9.0'},
+            },
+          )
+          .create();
+
+      await pubGet(
+        output: contains('+ foo 1.0.0'),
+        environment: {'_PUB_TEST_SDK_VERSION': '3.9.0'},
+      );
+    });
   }
+
+  test('compileTagPattern escapes regex special characters', () {
+    final regExp = compileTagPattern(r'[foo]+(bar)*.{{version}}^$');
+    expect(regExp.hasMatch(r'[foo]+(bar)*.1.2.3^$'), isTrue);
+    expect(regExp.hasMatch('f1.2.3'), isFalse);
+    final match = regExp.firstMatch(r'[foo]+(bar)*.1.2.3^$');
+    expect(match, isNotNull);
+    expect(match![1], '1.2.3');
+  });
 }