1
1
"""Mercurial source control management."""
2
2
3
+ import json
4
+ import os
5
+ import re
6
+ import shlex
3
7
import subprocess
8
+ from pathlib import Path
9
+ from tempfile import NamedTemporaryFile
4
10
from typing import ClassVar , List , MutableMapping , Optional
5
11
6
12
from bumpversion .exceptions import DirtyWorkingDirectoryError , SignedTagsError
7
13
from bumpversion .scm .models import LatestTagInfo , SCMConfig
8
14
from bumpversion .ui import get_indented_logger
9
- from bumpversion .utils import Pathlike , run_command
15
+ from bumpversion .utils import Pathlike , format_and_raise_error , is_subpath , run_command
10
16
11
17
logger = get_indented_logger (__name__ )
12
18
@@ -16,7 +22,6 @@ class Mercurial:
16
22
17
23
_TEST_AVAILABLE_COMMAND : ClassVar [List [str ]] = ["hg" , "root" ]
18
24
_COMMIT_COMMAND : ClassVar [List [str ]] = ["hg" , "commit" , "--logfile" ]
19
- _ALL_TAGS_COMMAND : ClassVar [List [str ]] = ["hg" , "log" , '--rev="tag()"' , '--template="{tags}\n "' ]
20
25
21
26
def __init__ (self , config : SCMConfig ):
22
27
self .config = config
@@ -50,14 +55,83 @@ def latest_tag_info(self) -> LatestTagInfo:
50
55
self ._latest_tag_info = LatestTagInfo (** info )
51
56
return self ._latest_tag_info
52
57
58
+ def get_all_tags (self ) -> List [str ]:
59
+ """Return all tags in a mercurial repository."""
60
+ try :
61
+ result = run_command (["hg" , "tags" , "-T" , "json" ])
62
+ tags = json .loads (result .stdout ) if result .stdout else []
63
+ return [tag ["tag" ] for tag in tags ]
64
+ except (
65
+ FileNotFoundError ,
66
+ PermissionError ,
67
+ NotADirectoryError ,
68
+ subprocess .CalledProcessError ,
69
+ ) as e :
70
+ format_and_raise_error (e )
71
+ return []
72
+
53
73
def add_path (self , path : Pathlike ) -> None :
54
74
"""Add a path to the Source Control Management repository."""
55
- pass
75
+ repository_root = self .latest_tag_info ().repository_root
76
+ if not (repository_root and is_subpath (repository_root , path )):
77
+ return
78
+
79
+ cwd = Path .cwd ()
80
+ temp_path = os .path .relpath (path , cwd )
81
+ try :
82
+ run_command (["hg" , "add" , str (temp_path )])
83
+ except subprocess .CalledProcessError as e :
84
+ format_and_raise_error (e )
56
85
57
86
def commit_and_tag (self , files : List [Pathlike ], context : MutableMapping , dry_run : bool = False ) -> None :
58
87
"""Commit and tag files to the repository using the configuration."""
88
+ if dry_run :
89
+ return
90
+
91
+ if self .config .commit :
92
+ for path in files :
93
+ self .add_path (path )
94
+
95
+ self .commit (context )
96
+
97
+ if self .config .tag :
98
+ tag_name = self .config .tag_name .format (** context )
99
+ tag_message = self .config .tag_message .format (** context )
100
+ tag (tag_name , sign = self .config .sign_tags , message = tag_message )
101
+
102
+ # for m_tag_name in self.config.moveable_tags:
103
+ # moveable_tag(m_tag_name)
104
+
105
+ def commit (self , context : MutableMapping ) -> None :
106
+ """Commit the changes."""
107
+ extra_args = shlex .split (self .config .commit_args ) if self .config .commit_args else []
108
+
109
+ current_version = context .get ("current_version" , "" )
110
+ new_version = context .get ("new_version" , "" )
111
+ commit_message = self .config .message .format (** context )
59
112
60
- def tag (self , name : str , sign : bool = False , message : Optional [str ] = None ) -> None :
113
+ if not current_version : # pragma: no-coverage
114
+ logger .warning ("No current version given, using an empty string." )
115
+ if not new_version : # pragma: no-coverage
116
+ logger .warning ("No new version given, using an empty string." )
117
+
118
+ with NamedTemporaryFile ("wb" , delete = False ) as f :
119
+ f .write (commit_message .encode ("utf-8" ))
120
+
121
+ env = os .environ .copy ()
122
+ env ["BUMPVERSION_CURRENT_VERSION" ] = current_version
123
+ env ["BUMPVERSION_NEW_VERSION" ] = new_version
124
+
125
+ try :
126
+ cmd = [* self ._COMMIT_COMMAND , f .name , * extra_args ]
127
+ run_command (cmd , env = env )
128
+ except (subprocess .CalledProcessError , TypeError ) as exc : # pragma: no-coverage
129
+ format_and_raise_error (exc )
130
+ finally :
131
+ os .unlink (f .name )
132
+
133
+ @staticmethod
134
+ def tag (name : str , sign : bool = False , message : Optional [str ] = None ) -> None :
61
135
"""
62
136
Create a tag of the new_version in VCS.
63
137
@@ -72,14 +146,10 @@ def tag(self, name: str, sign: bool = False, message: Optional[str] = None) -> N
72
146
Raises:
73
147
SignedTagsError: If ``sign`` is ``True``
74
148
"""
75
- command = ["hg" , "tag" , name ]
76
- if sign :
77
- raise SignedTagsError ("Mercurial does not support signed tags." )
78
- if message :
79
- command += ["--message" , message ]
80
- run_command (command )
81
-
82
- def assert_nondirty (self ) -> None :
149
+ tag (name , sign = sign , message = message )
150
+
151
+ @staticmethod
152
+ def assert_nondirty () -> None :
83
153
"""Assert that the working directory is clean."""
84
154
assert_nondirty ()
85
155
@@ -95,21 +165,53 @@ def commit_info(config: SCMConfig) -> dict:
95
165
A dictionary containing information about the latest commit.
96
166
"""
97
167
tag_pattern = config .tag_name .replace ("{new_version}" , ".*" )
98
- info = dict .fromkeys (["dirty" , "commit_sha" , "distance_to_latest_tag" , "current_version" , "current_tag" ])
168
+ info = dict .fromkeys (
169
+ [
170
+ "dirty" ,
171
+ "commit_sha" ,
172
+ "distance_to_latest_tag" ,
173
+ "current_version" ,
174
+ "current_tag" ,
175
+ "branch_name" ,
176
+ "short_branch_name" ,
177
+ "repository_root" ,
178
+ ]
179
+ )
180
+
99
181
info ["distance_to_latest_tag" ] = 0
100
- result = run_command (["hg" , "log" , "-r" , f"tag('re:{ tag_pattern } ')" , "--template" , "{latesttag}\n " ])
101
- result .check_returncode ()
182
+ result = run_command (["hg" , "log" , "-r" , f"tag('re:{ tag_pattern } ')" , "-T" , "json" ])
183
+ repo_path = run_command (["hg" , "root" ]).stdout .strip ()
184
+
185
+ output_info = parse_commit_log (result .stdout , config )
186
+ info |= output_info
102
187
103
- if result .stdout :
104
- tag_string = result .stdout .splitlines (keepends = False )[- 1 ]
105
- info ["current_version" ] = config .get_version_from_tag (tag_string )
106
- else :
188
+ if not output_info :
107
189
logger .debug ("No tags found" )
108
190
191
+ info ["repository_root" ] = Path (repo_path )
109
192
info ["dirty" ] = len (run_command (["hg" , "status" , "-mard" ]).stdout ) != 0
110
193
return info
111
194
112
195
196
+ def parse_commit_log (log_string : str , config : SCMConfig ) -> dict :
197
+ """Parse the commit log string."""
198
+ output_info = json .loads (log_string ) if log_string else {}
199
+ if not output_info :
200
+ return {}
201
+ first_rev = output_info [0 ]
202
+ branch_name = first_rev ["branch" ]
203
+ short_branch_name = re .sub (r"([^a-zA-Z0-9]*)" , "" , branch_name ).lower ()[:20 ]
204
+
205
+ return {
206
+ "current_version" : config .get_version_from_tag (first_rev ["tags" ][0 ]),
207
+ "current_tag" : first_rev ["tags" ][0 ],
208
+ "commit_sha" : first_rev ["node" ],
209
+ "distance_to_latest_tag" : 0 ,
210
+ "branch_name" : branch_name ,
211
+ "short_branch_name" : short_branch_name ,
212
+ }
213
+
214
+
113
215
def tag (name : str , sign : bool = False , message : Optional [str ] = None ) -> None :
114
216
"""
115
217
Create a tag of the new_version in VCS.
@@ -135,7 +237,6 @@ def tag(name: str, sign: bool = False, message: Optional[str] = None) -> None:
135
237
136
238
def assert_nondirty () -> None :
137
239
"""Assert that the working directory is clean."""
138
- print (run_command (["hg" , "status" , "-mard" ]).stdout .splitlines ())
139
240
if lines := [
140
241
line .strip ()
141
242
for line in run_command (["hg" , "status" , "-mard" ]).stdout .splitlines ()
0 commit comments