Coverage for repo_ctx/providers/detector.py: 93%

1"""Provider detection from repository paths."""

2 from typing import Optional

3

4

5 class ProviderDetector:

6 """Detect provider type from repository path."""

7

8 @staticmethod

9 def detect(path: str, default: Optional[str] = None) -> str:

10 """

11 Detect provider from path format.

12

13 Examples:

14 "owner/repo" -> "github" (2 parts, typical GitHub format)

15 "group/subgroup/project" -> "gitlab" (3+ parts)

16 "/absolute/path" -> "local"

17 "./relative/path" -> "local"

18 "../path" -> "local"

19 "~/path" -> "local"

20 "pypi:package" -> "pypi" (future)

21

22 Args:

23 path: Repository path to analyze

24 default: Default provider if detection is ambiguous

25

26 Returns:

27 Provider type: "gitlab", "github", "local", etc.

28

29 Raises:

30 ValueError: Cannot detect provider from path

31

32 Note:

33 For ambiguous cases (owner/repo could be GitLab or GitHub),

34 the default provider is used if provided, otherwise "github" is assumed.

35 """

36 # Handle explicit protocol prefixes

37 if "://" in path:

38 protocol = path.split("://")[0]

39 return protocol # gitlab://, github://, local://, etc.

40

41 # Local filesystem paths

42 if path.startswith("/") or path.startswith("~"):

43 return "local"

44 if path.startswith("."):

45 return "local"

46

47 # Git-style paths

48 parts = path.split("/")

49

50 # Single part is invalid

51 if len(parts) == 1:

52 raise ValueError(

53 f"Cannot detect provider from path: {path}. "

54 f"Expected format: owner/repo, group/project, or /path/to/repo"

55 )

56

57 # Two parts: owner/repo (typically GitHub)

58 if len(parts) == 2:

59 # Could be GitLab or GitHub, use default or assume GitHub

60 if default:

61 return default

62 return "github"

63

64 # Three or more parts: group/subgroup/project (typically GitLab)

65 if len(parts) >= 3:

66 return "gitlab"

67

68 # Fallback to default or error

69 if default:

70 return default

71

72 raise ValueError(f"Cannot detect provider from path: {path}")

73

74 @staticmethod

75 def normalize_path(path: str, provider: str) -> str:

76 """

77 Normalize path for given provider.

78

79 Removes protocol prefixes and ensures correct format.

80

81 Args:

82 path: Repository path (may include protocol)

83 provider: Provider type

84

85 Returns:

86 Normalized path without protocol

87

88 Examples:

89 gitlab://group/project -> group/project

90 github://owner/repo -> owner/repo

91 local:///path/to/repo -> /path/to/repo

92 """

93 # Remove protocol if present

94 if "://" in path:

95 protocol, rest = path.split("://", 1)

96 return rest

97

98 return path

99

100 @staticmethod

101 def to_library_id(path: str, provider: str) -> str:

102 """

103 Convert path and provider to library_id URI format.

104

105 Args:

106 path: Repository path

107 provider: Provider type

108

109 Returns:

110 Library ID in URI format

111

112 Examples:

113 ("group/project", "gitlab") -> "gitlab://group/project"

114 ("owner/repo", "github") -> "github://owner/repo"

115 ("/path/to/repo", "local") -> "local:///path/to/repo"

116 """

117 normalized = ProviderDetector.normalize_path(path, provider)

118

119 if provider == "local":

120 # Ensure absolute path starts with /

121 if not normalized.startswith("/"):

122 raise ValueError(f"Local paths must be absolute: {normalized}")

123 return f"local://{normalized}"

124

125 return f"{provider}://{normalized}"

126

127 @staticmethod

128 def from_library_id(library_id: str) -> tuple[str, str]:

129 """

130 Parse library_id URI into provider and path.

131

132 Args:

133 library_id: Library ID in URI format

134

135 Returns:

136 Tuple of (provider, path)

137

138 Examples:

139 "gitlab://group/project" -> ("gitlab", "group/project")

140 "github://owner/repo" -> ("github", "owner/repo")

141 "local:///path/to/repo" -> ("local", "/path/to/repo")

142

143 Raises:

144 ValueError: Invalid library_id format

145 """

146 if "://" not in library_id:

147 raise ValueError(f"Invalid library_id format: {library_id}")

148

149 provider, path = library_id.split("://", 1)

150

151 return provider, path