Coverage for gsb/rewind.py: 100%

28 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-05-09 21:05 +0000

1"""Functionality for restoring to an old backup""" 

2 

3import logging 

4from pathlib import Path 

5 

6from . import _git, backup, manifest 

7from .logging import IMPORTANT 

8 

9LOGGER = logging.getLogger(__name__) 

10 

11 

12def generate_restore_tag_name(revision: str) -> str: 

13 """Generate a new calver-ish tag name 

14 

15 Parameters 

16 ---------- 

17 revision : str 

18 The commit hash or tag name of the backup to restore 

19 

20 Returns 

21 ------- 

22 str 

23 A tag name that indicates both the time a backup was restored and the 

24 identifier of the original revision 

25 """ 

26 return f"{backup.generate_tag_name()}.restore_of_{revision}" 

27 

28 

29def restore_backup( 

30 repo_root: Path, revision: str, keep_gsb_files: bool = True, hard: bool = False 

31) -> str: 

32 """Rewind to a previous backup state and create a new backup 

33 

34 Parameters 

35 ---------- 

36 repo_root : Path 

37 The directory containing the GSB-managed repo 

38 revision : str 

39 The commit hash or tag name of the backup to restore 

40 keep_gsb_files : bool, optional 

41 By default, `.gsb_manifest` and `.gitignore` *will not* be restored 

42 (that is, the latest versions will be kept). To override this behavior, 

43 pass in `keep_gsb_files = False`. 

44 hard : bool, optional 

45 By default, any backups after the specified revision will be kept in 

46 the history (with the specified restore point just copied to the top 

47 of the stack). If you want to *fully erase* all progress after the 

48 restore point (backed up or loose), pass in `hard = True` and then run 

49 `git gc --aggressive --prune=now` afterward. 

50 

51 Returns 

52 ------- 

53 str 

54 The name of the restored backup 

55 

56 Notes 

57 ----- 

58 - Before creating the backup, any un-backed up changes will first be backed up 

59 unless `hard = True` is specified. 

60 - When restoring with `hard = True`, **unsaved changes** to `.gsb_manifest` 

61 and `.gitignore` will **not** be kept, even with `keep_gsb_files = True`. 

62 Instead, the restored backup will contain the GSB files from the most 

63 recent backup (tagged or otherwise). 

64 

65 Raises 

66 ------ 

67 OSError 

68 If the specified repo does not exist or is not a GSB-managed repo 

69 ValueError 

70 If the specified revision does not exist 

71 """ 

72 _git.show(repo_root, revision) # ensure revision exists 

73 

74 orig_head = _git.show(repo_root, "HEAD").hash # type: ignore[union-attr] 

75 

76 if hard: 

77 _git.add(repo_root, manifest.Manifest.of(repo_root).patterns) 

78 else: 

79 LOGGER.log( 

80 IMPORTANT, "Backing up any unsaved changes before rewinding to %s", revision 

81 ) 

82 try: 

83 orig_head = backup.create_backup( 

84 repo_root, 

85 f"Backing up state before rewinding to {revision}", 

86 ) 

87 except ValueError: # nothing to back up 

88 pass 

89 

90 _git.reset(repo_root, revision, hard=True) 

91 if keep_gsb_files: 

92 _git.checkout_files(repo_root, orig_head, backup.REQUIRED_FILES) 

93 if hard: 

94 try: # on the off chance that the gsb files changed, commit them 

95 backup.create_backup( 

96 repo_root, commit_message="Cherry-picking changes to the gsb files" 

97 ) 

98 except ValueError: # this is actually the more likely outcome 

99 pass 

100 return revision 

101 

102 _git.reset(repo_root, orig_head, hard=False) 

103 return backup.create_backup( 

104 repo_root, 

105 f"Restored to {revision}", 

106 tag_name=generate_restore_tag_name(revision), 

107 )