14
14
from dataclasses import dataclass
15
15
from datetime import datetime , timezone
16
16
from pathlib import Path
17
- from typing import TYPE_CHECKING , Callable , Iterable , NamedTuple
17
+ from typing import TYPE_CHECKING , Callable , NamedTuple
18
18
19
19
from pdm .backend ._vendor .packaging .version import Version
20
20
29
29
@dataclass (frozen = True )
30
30
class Config :
31
31
tag_regex : re .Pattern
32
+ tag_filter : str | None
32
33
33
34
34
35
def _subprocess_call (
@@ -164,28 +165,27 @@ def tag_to_version(config: Config, tag: str) -> Version:
164
165
return Version (version )
165
166
166
167
167
- def tags_to_versions (config : Config , tags : Iterable [str ]) -> list [Version ]:
168
- """
169
- take tags that might be prefixed with a keyword and return only the version part
170
- :param tags: an iterable of tags
171
- :param config: optional configuration object
172
- """
173
- return [tag_to_version (config , tag ) for tag in tags if tag ]
174
-
175
-
176
168
def git_parse_version (root : StrPath , config : Config ) -> SCMVersion | None :
177
- GIT = shutil .which ("git" )
178
- if not GIT :
169
+ git = shutil .which ("git" )
170
+ if not git :
179
171
return None
180
172
181
- ret , repo , _ = _subprocess_call ([GIT , "rev-parse" , "--show-toplevel" ], root )
173
+ ret , repo , _ = _subprocess_call ([git , "rev-parse" , "--show-toplevel" ], root )
182
174
if ret or not repo :
183
175
return None
184
176
185
177
if os .path .isfile (os .path .join (repo , ".git/shallow" )):
186
178
warnings .warn (f"{ repo !r} is shallow and may cause errors" )
187
- describe_cmd = [GIT , "describe" , "--dirty" , "--tags" , "--long" , "--match" , "*.*" ]
188
- ret , output , err = _subprocess_call (describe_cmd , repo )
179
+ describe_cmd = [
180
+ git ,
181
+ "describe" ,
182
+ "--dirty" ,
183
+ "--tags" ,
184
+ "--long" ,
185
+ "--match" ,
186
+ config .tag_filter or "*.*" ,
187
+ ]
188
+ ret , output , _ = _subprocess_call (describe_cmd , repo )
189
189
branch = _git_get_branch (repo )
190
190
191
191
if ret :
@@ -201,54 +201,44 @@ def git_parse_version(root: StrPath, config: Config) -> SCMVersion | None:
201
201
return meta (config , tag , number or None , dirty , node , branch )
202
202
203
203
204
- def get_latest_normalizable_tag (root : StrPath ) -> str :
205
- # Gets all tags containing a '.' from oldest to newest
206
- cmd = [
207
- "hg" ,
208
- "log" ,
209
- "-r" ,
210
- "ancestors(.) and tag('re:\\ .')" ,
211
- "--template" ,
212
- "{tags}\n " ,
213
- ]
214
- _ , output , _ = _subprocess_call (cmd , root )
215
- outlines = output .split ()
216
- if not outlines :
217
- return "null"
218
- tag = outlines [- 1 ].split ()[- 1 ]
219
- return tag
204
+ def get_distance_revset (tag : str | None ) -> str :
205
+ return (
206
+ "(branch(.)" # look for revisions in this branch only
207
+ " and {rev}::." # after the last tag
208
+ # ignore commits that only modify .hgtags and nothing else:
209
+ " and (merge() or file('re:^(?!\\ .hgtags).*$'))"
210
+ " and not {rev})" # ignore the tagged commit itself
211
+ ).format (rev = f"tag({ tag !r} )" if tag is not None else "null" )
220
212
221
213
222
- def hg_get_graph_distance (root : StrPath , rev1 : str , rev2 : str = "." ) -> int :
223
- cmd = ["hg" , "log" , "-q" , "-r" , f" { rev1 } :: { rev2 } " ]
214
+ def hg_get_graph_distance (root : StrPath , tag : str | None ) -> int :
215
+ cmd = ["hg" , "log" , "-q" , "-r" , get_distance_revset ( tag ) ]
224
216
_ , out , _ = _subprocess_call (cmd , root )
225
- return len (out .strip ().splitlines ()) - 1
217
+ return len (out .strip ().splitlines ())
226
218
227
219
228
220
def _hg_tagdist_normalize_tagcommit (
229
- config : Config , root : StrPath , tag : str , dist : int , node : str , branch : str
221
+ config : Config ,
222
+ root : StrPath ,
223
+ tag : str ,
224
+ dist : int ,
225
+ node : str ,
226
+ branch : str ,
227
+ dirty : bool ,
230
228
) -> SCMVersion :
231
- dirty = node .endswith ("+" )
232
- node = "h" + node .strip ("+" )
233
-
234
229
# Detect changes since the specified tag
235
- revset = (
236
- "(branch(.)" # look for revisions in this branch only
237
- " and tag({tag!r})::." # after the last tag
238
- # ignore commits that only modify .hgtags and nothing else:
239
- " and (merge() or file('re:^(?!\\ .hgtags).*$'))"
240
- " and not tag({tag!r}))" # ignore the tagged commit itself
241
- ).format (tag = tag )
242
230
if tag != "0.0" :
243
231
_ , commits , _ = _subprocess_call (
244
- ["hg" , "log" , "-r" , revset , "--template" , "{node|short}" ],
232
+ ["hg" , "log" , "-r" , get_distance_revset ( tag ) , "--template" , "{node|short}" ],
245
233
root ,
246
234
)
247
235
else :
248
236
commits = "True"
249
237
250
238
if commits or dirty :
251
- return meta (config , tag , distance = dist , node = node , dirty = dirty , branch = branch )
239
+ return meta (
240
+ config , tag , distance = dist or None , node = node , dirty = dirty , branch = branch
241
+ )
252
242
else :
253
243
return meta (config , tag )
254
244
@@ -280,32 +270,40 @@ def _bump_regex(version: str) -> str:
280
270
281
271
282
272
def hg_parse_version (root : StrPath , config : Config ) -> SCMVersion | None :
283
- if not shutil .which ("hg" ):
273
+ hg = shutil .which ("hg" )
274
+ if not hg :
284
275
return None
285
- _ , output , _ = _subprocess_call ("hg id -i -b -t" , root )
286
- identity_data = output .split ()
287
- if not identity_data :
288
- return None
289
- node = identity_data .pop (0 )
290
- branch = identity_data .pop (0 )
291
- if "tip" in identity_data :
292
- # tip is not a real tag
293
- identity_data .remove ("tip" )
294
- tags = tags_to_versions (config , identity_data )
295
- dirty = node [- 1 ] == "+"
296
- if tags :
297
- return meta (config , tags [0 ], dirty = dirty , branch = branch )
298
-
299
- if node .strip ("+" ) == "0" * 12 :
300
- return meta (config , "0.0" , dirty = dirty , branch = branch )
301
276
277
+ tag_filter = config .tag_filter or "\\ ."
278
+ _ , output , _ = _subprocess_call (
279
+ [
280
+ hg ,
281
+ "log" ,
282
+ "-r" ,
283
+ "." ,
284
+ "--template" ,
285
+ f"{{latesttag(r're:{ tag_filter } ')}}-{{node|short}}-{{branch}}" ,
286
+ ],
287
+ root ,
288
+ )
289
+ tag : str | None
290
+ tag , node , branch = output .rsplit ("-" , 2 )
291
+ # If no tag exists passes the tag filter.
292
+ if tag == "null" :
293
+ tag = None
294
+
295
+ _ , id_output , _ = _subprocess_call (
296
+ [hg , "id" , "-i" ],
297
+ root ,
298
+ )
299
+ dirty = id_output .endswith ("+" )
302
300
try :
303
- tag = get_latest_normalizable_tag (root )
304
301
dist = hg_get_graph_distance (root , tag )
305
- if tag == "null" :
302
+ if tag is None :
306
303
tag = "0.0"
307
- dist = int (dist ) + 1
308
- return _hg_tagdist_normalize_tagcommit (config , root , tag , dist , node , branch )
304
+ return _hg_tagdist_normalize_tagcommit (
305
+ config , root , tag , dist , node , branch , dirty = dirty
306
+ )
309
307
except ValueError :
310
308
return None # unpacking failed, old hg
311
309
@@ -332,11 +330,15 @@ def get_version_from_scm(
332
330
root : str | Path ,
333
331
* ,
334
332
tag_regex : str | None = None ,
333
+ tag_filter : str | None = None ,
335
334
version_formatter : Callable [[SCMVersion ], str ] | None = None ,
336
335
) -> str | None :
337
- config = Config (tag_regex = re .compile (tag_regex ) if tag_regex else DEFAULT_TAG_REGEX )
336
+ config = Config (
337
+ tag_regex = re .compile (tag_regex ) if tag_regex else DEFAULT_TAG_REGEX ,
338
+ tag_filter = tag_filter ,
339
+ )
338
340
for func in (git_parse_version , hg_parse_version ):
339
- version = func (root , config ) # type: ignore
341
+ version = func (root , config )
340
342
if version :
341
343
if version_formatter is None :
342
344
version_formatter = format_version
0 commit comments