0
# SCM Integration
1
2
Source control management integration supporting Git and Mercurial for automated commits, tagging, and repository state management. Provides comprehensive SCM operations with repository status checking, dirty state validation, and automated commit/tag workflows.
3
4
## Capabilities
5
6
### SCM Information and Status
7
8
Classes for managing source control information and repository state.
9
10
```python { .api }
11
class SCMInfo:
12
"""
13
Information about the current source code management system.
14
15
Provides comprehensive repository state including current commit,
16
branch information, tag distance, and version detection from SCM.
17
"""
18
19
def __init__(
20
self,
21
tool: Optional[str] = None,
22
commit_sha: Optional[str] = None,
23
distance_to_latest_tag: int = 0,
24
current_version: str = "0.0.0",
25
branch_name: Optional[str] = None,
26
repository_root: Optional[Path] = None,
27
short_branch_name: Optional[str] = None
28
) -> None:
29
"""
30
Initialize SCM information.
31
32
Args:
33
tool: SCM tool name ('git', 'hg', etc.)
34
commit_sha: Current commit SHA
35
distance_to_latest_tag: Number of commits since latest tag
36
current_version: Current version string
37
branch_name: Full branch name
38
repository_root: Path to repository root
39
short_branch_name: Short branch name
40
"""
41
42
@property
43
def is_dirty(self) -> bool:
44
"""Whether repository has uncommitted changes."""
45
46
@property
47
def latest_tag_info(self) -> Optional[LatestTagInfo]:
48
"""Information about the most recent tag."""
49
50
class LatestTagInfo:
51
"""
52
Information about the latest repository tag.
53
54
Contains tag name, commit SHA, and version information
55
extracted from the most recent repository tag.
56
"""
57
58
tag_name: str
59
commit_sha: str
60
version: Optional[str] = None
61
62
@property
63
def is_version_tag(self) -> bool:
64
"""Whether tag represents a version."""
65
66
class SCMConfig:
67
"""
68
Configuration for SCM operations.
69
70
Contains settings for commit behavior, tag creation,
71
message templates, and repository interaction options.
72
"""
73
74
commit: bool = False
75
tag: bool = False
76
commit_args: str = ""
77
message: str = "Bump version: {current_version} → {new_version}"
78
tag_name: str = "v{new_version}"
79
tag_message: str = "Bump version: {current_version} → {new_version}"
80
sign_tags: bool = False
81
allow_dirty: bool = False
82
```
83
84
### Git Implementation
85
86
Comprehensive Git integration with full repository management capabilities.
87
88
```python { .api }
89
class Git:
90
"""
91
Git implementation for SCM operations.
92
93
Provides complete Git integration including repository status checking,
94
commit creation, tag management, and branch operations.
95
"""
96
97
def __init__(self, repo_root: Path) -> None:
98
"""
99
Initialize Git integration.
100
101
Args:
102
repo_root: Path to Git repository root
103
"""
104
105
def is_dirty(self) -> bool:
106
"""
107
Check if repository has uncommitted changes.
108
109
Returns:
110
True if repository has staged or unstaged changes
111
"""
112
113
def commit_and_tag(
114
self,
115
message: str,
116
tag_name: Optional[str] = None,
117
tag_message: Optional[str] = None,
118
sign_tags: bool = False,
119
commit_args: str = ""
120
) -> None:
121
"""
122
Create commit and optional tag for version changes.
123
124
Stages all modified files, creates commit with specified message,
125
and optionally creates annotated tag.
126
127
Args:
128
message: Commit message
129
tag_name: Tag name to create (optional)
130
tag_message: Tag annotation message
131
sign_tags: Whether to sign tags with GPG
132
commit_args: Additional arguments for git commit
133
134
Raises:
135
subprocess.CalledProcessError: Git command failed
136
"""
137
138
def get_current_info(self) -> SCMInfo:
139
"""
140
Get comprehensive repository information.
141
142
Returns:
143
SCMInfo object with current repository state
144
"""
145
146
def get_latest_tag_info(self) -> Optional[LatestTagInfo]:
147
"""
148
Get information about the most recent tag.
149
150
Returns:
151
LatestTagInfo object or None if no tags exist
152
"""
153
154
def get_branch_name(self) -> Optional[str]:
155
"""
156
Get current branch name.
157
158
Returns:
159
Current branch name or None if detached HEAD
160
"""
161
162
def add_path(self, path: Path) -> None:
163
"""
164
Add file or directory to Git staging area.
165
166
Args:
167
path: Path to add to staging area
168
"""
169
170
def tag_exists(self, tag_name: str) -> bool:
171
"""
172
Check if tag exists in repository.
173
174
Args:
175
tag_name: Tag name to check
176
177
Returns:
178
True if tag exists
179
"""
180
181
class Mercurial:
182
"""
183
Mercurial implementation for SCM operations.
184
185
Provides Mercurial integration with repository status checking,
186
commit creation, and tag management capabilities.
187
"""
188
189
def __init__(self, repo_root: Path) -> None:
190
"""Initialize Mercurial integration."""
191
192
def is_dirty(self) -> bool:
193
"""Check if repository has uncommitted changes."""
194
195
def commit_and_tag(
196
self,
197
message: str,
198
tag_name: Optional[str] = None,
199
tag_message: Optional[str] = None
200
) -> None:
201
"""Create commit and optional tag."""
202
203
def get_current_info(self) -> SCMInfo:
204
"""Get repository information."""
205
```
206
207
### SCM Utility Functions
208
209
Helper functions for SCM detection and management.
210
211
```python { .api }
212
def get_scm_info(repo_path: Optional[Path] = None) -> Optional[SCMInfo]:
213
"""
214
Detect and get SCM information for repository.
215
216
Automatically detects Git or Mercurial repositories and returns
217
comprehensive information about repository state.
218
219
Args:
220
repo_path: Path to repository (defaults to current directory)
221
222
Returns:
223
SCMInfo object or None if no SCM detected
224
"""
225
226
def detect_scm_type(repo_path: Path) -> Optional[str]:
227
"""
228
Detect SCM type for repository.
229
230
Args:
231
repo_path: Path to check for SCM
232
233
Returns:
234
SCM type ('git', 'hg') or None
235
"""
236
```
237
238
## SCM Configuration Examples
239
240
### Basic SCM Settings
241
242
```toml
243
[tool.bumpversion]
244
current_version = "1.0.0"
245
commit = true
246
tag = true
247
allow_dirty = false
248
message = "Bump version: {current_version} → {new_version}"
249
tag_name = "v{new_version}"
250
tag_message = "Release {new_version}"
251
commit_args = "--no-verify"
252
sign_tags = false
253
```
254
255
### Advanced SCM Configuration
256
257
```toml
258
[tool.bumpversion]
259
# Commit settings
260
commit = true
261
commit_args = "--no-verify --gpg-sign"
262
message = """
263
Release version {new_version}
264
265
Changes:
266
- Updated version from {current_version} to {new_version}
267
- Automated by bump-my-version on {now:%Y-%m-%d}
268
"""
269
270
# Tag settings
271
tag = true
272
tag_name = "release-{new_version}"
273
tag_message = "Release {new_version} ({now:%Y-%m-%d})"
274
sign_tags = true
275
276
# Repository settings
277
allow_dirty = false
278
```
279
280
### Conditional SCM Operations
281
282
```python
283
from bumpversion.config import get_configuration
284
285
# Load configuration
286
config = get_configuration()
287
288
# Only commit in CI environment
289
import os
290
if os.getenv("CI"):
291
config.commit = True
292
config.tag = True
293
else:
294
config.commit = False
295
config.tag = False
296
```
297
298
## Usage Examples
299
300
### Basic SCM Operations
301
302
```python
303
from bumpversion.scm.git import Git
304
from pathlib import Path
305
306
# Initialize Git integration
307
git = Git(Path.cwd())
308
309
# Check repository status
310
if git.is_dirty():
311
print("Repository has uncommitted changes")
312
313
# Get repository information
314
scm_info = git.get_current_info()
315
print(f"Current branch: {scm_info.branch_name}")
316
print(f"Current commit: {scm_info.commit_sha}")
317
print(f"Distance to latest tag: {scm_info.distance_to_latest_tag}")
318
319
# Create commit and tag
320
git.commit_and_tag(
321
message="Bump version: 1.0.0 → 1.0.1",
322
tag_name="v1.0.1",
323
tag_message="Release version 1.0.1"
324
)
325
```
326
327
### Repository State Checking
328
329
```python
330
from bumpversion.scm.models import SCMInfo
331
from bumpversion.scm.git import Git
332
333
# Get comprehensive repository information
334
git = Git(Path.cwd())
335
scm_info = git.get_current_info()
336
337
print(f"SCM Tool: {scm_info.tool}")
338
print(f"Repository Root: {scm_info.repository_root}")
339
print(f"Current Branch: {scm_info.branch_name}")
340
print(f"Is Dirty: {scm_info.is_dirty}")
341
342
# Check latest tag information
343
latest_tag = git.get_latest_tag_info()
344
if latest_tag:
345
print(f"Latest Tag: {latest_tag.tag_name}")
346
print(f"Tag Commit: {latest_tag.commit_sha}")
347
print(f"Is Version Tag: {latest_tag.is_version_tag}")
348
```
349
350
### Automated Commit and Tag Creation
351
352
```python
353
from bumpversion.scm.git import Git
354
from bumpversion.context import get_context
355
from bumpversion.config import get_configuration
356
357
# Load configuration and create context
358
config = get_configuration()
359
context = get_context(config)
360
361
# Initialize Git
362
git = Git(Path.cwd())
363
364
# Check if repository is clean (if required)
365
if not config.allow_dirty and git.is_dirty():
366
raise DirtyWorkingDirectoryError("Repository has uncommitted changes")
367
368
# Format commit and tag messages
369
commit_message = config.message.format(**context)
370
tag_name = config.tag_name.format(**context) if config.tag else None
371
tag_message = config.tag_message.format(**context) if config.tag else None
372
373
# Create commit and tag
374
git.commit_and_tag(
375
message=commit_message,
376
tag_name=tag_name,
377
tag_message=tag_message,
378
sign_tags=config.sign_tags,
379
commit_args=config.commit_args
380
)
381
```
382
383
### Branch and Tag Management
384
385
```python
386
from bumpversion.scm.git import Git
387
388
git = Git(Path.cwd())
389
390
# Get current branch
391
branch = git.get_branch_name()
392
print(f"Current branch: {branch}")
393
394
# Check if tag exists
395
tag_name = "v1.0.1"
396
if git.tag_exists(tag_name):
397
print(f"Tag {tag_name} already exists")
398
else:
399
print(f"Tag {tag_name} is available")
400
401
# Add specific files to staging
402
from pathlib import Path
403
git.add_path(Path("setup.py"))
404
git.add_path(Path("src/mypackage/__init__.py"))
405
```
406
407
### SCM Integration with Configuration
408
409
```python
410
from bumpversion.config import get_configuration
411
from bumpversion.scm.git import Git
412
from bumpversion.context import get_context
413
414
# Load configuration with SCM settings
415
config = get_configuration(
416
commit=True,
417
tag=True,
418
message="Release {new_version}",
419
tag_name="v{new_version}",
420
allow_dirty=False
421
)
422
423
# Get SCM instance
424
git = Git(Path.cwd())
425
426
# Validate repository state
427
if not config.allow_dirty and git.is_dirty():
428
print("Repository must be clean for version bump")
429
exit(1)
430
431
# Get context for templating
432
context = get_context(config)
433
context.update({
434
"current_version": "1.0.0",
435
"new_version": "1.0.1"
436
})
437
438
# Execute SCM operations if configured
439
if config.commit or config.tag:
440
git.commit_and_tag(
441
message=config.message.format(**context),
442
tag_name=config.tag_name.format(**context) if config.tag else None,
443
tag_message=config.tag_message.format(**context) if config.tag else None,
444
sign_tags=config.sign_tags,
445
commit_args=config.commit_args
446
)
447
```
448
449
### Error Handling
450
451
```python
452
from bumpversion.scm.git import Git
453
from bumpversion.exceptions import DirtyWorkingDirectoryError
454
import subprocess
455
456
try:
457
git = Git(Path.cwd())
458
459
# Check repository state
460
if git.is_dirty() and not allow_dirty:
461
raise DirtyWorkingDirectoryError(
462
"Repository has uncommitted changes. "
463
"Use --allow-dirty to override."
464
)
465
466
# Attempt SCM operations
467
git.commit_and_tag(
468
message="Bump version",
469
tag_name="v1.0.1",
470
tag_message="Release 1.0.1"
471
)
472
473
except subprocess.CalledProcessError as e:
474
print(f"Git command failed: {e}")
475
print(f"Command: {e.cmd}")
476
print(f"Output: {e.output}")
477
478
except DirtyWorkingDirectoryError as e:
479
print(f"Repository state error: {e}")
480
```