beta-cut.py (8498B)
1 #!/usr/bin/python3 2 # This Source Code Form is subject to the terms of the Mozilla Public 3 # License, v. 2.0. If a copy of the MPL was not distributed with this 4 # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 6 import os 7 import re 8 import subprocess 9 import sys 10 11 # Constants 12 MOBILE_ANDROID_DIR = os.path.abspath(os.path.dirname(__file__)) 13 CHANGELOG_FILE = os.path.join( 14 MOBILE_ANDROID_DIR, "android-components/docs/changelog.md" 15 ) 16 SOURCE_JSON = os.path.join( 17 MOBILE_ANDROID_DIR, "../../services/settings/dumps/main/search-telemetry-v2.json" 18 ) 19 TARGET_JSON = os.path.join( 20 MOBILE_ANDROID_DIR, 21 "android-components/components/feature/search/src/main/assets/search/search_telemetry_v2.json", 22 ) 23 EXPIRED_STRING_VERSION_OFFSET = 3 24 25 26 def check_ripgrep_installed(): 27 """Check if ripgrep (rg) is installed.""" 28 try: 29 subprocess.run(["rg", "--version"], capture_output=True, check=True) 30 except FileNotFoundError: 31 print( 32 "ERROR: ripgrep (rg) is not installed. Please install ripgrep and try again." 33 ) 34 print( 35 "See installation instructions here: https://github.com/BurntSushi/ripgrep?tab=readme-ov-file#installation" 36 ) 37 sys.exit(1) 38 39 40 def check_uncommitted_changes(): 41 """Check for uncommitted changes in the git repository.""" 42 result = subprocess.run( 43 ["git", "status", "--porcelain", "--untracked-files=no"], 44 capture_output=True, 45 text=True, 46 check=False, 47 ) 48 if result.stdout.strip(): 49 print("ERROR: Please commit changes before continuing.") 50 sys.exit(1) 51 52 53 def get_bug_id(): 54 """Get BUG_ID from script arguments.""" 55 if len(sys.argv) < 2: 56 print("Usage: python script.py BUG_ID") 57 sys.exit(1) 58 return sys.argv[1] 59 60 61 def get_previous_version(): 62 """Extract the previous version number from the changelog.""" 63 with open(CHANGELOG_FILE) as file: 64 content = file.read() 65 match = re.search(r"# (\d+)\.0 \(In Development\)", content) 66 if not match: 67 print( 68 "ERROR: Unable to extract the previous version number from the changelog file." 69 ) 70 sys.exit(1) 71 return int(match.group(1)) 72 73 74 def update_changelog(previous_version, new_version): 75 """Update the changelog with the new version number.""" 76 with open(CHANGELOG_FILE) as file: 77 content = file.read() 78 updated_content = content.replace( 79 f"# {previous_version}.0 (In Development)", 80 f"# {new_version}.0 (In Development)\n\n# {previous_version}.0", 81 ) 82 with open(CHANGELOG_FILE, "w") as file: 83 file.write(updated_content) 84 85 86 def find_expired_strings(expired_string_version): 87 """Find strings to be removed.""" 88 rg_command = [ 89 "rg", 90 "-g", 91 "**/values/**", 92 "-U", 93 f'(<!--.*-->[\\r\\n\\s]*)?<string[^>]*moz:removedIn="{expired_string_version}"[^>]*>.*?</string>', 94 MOBILE_ANDROID_DIR, 95 ] 96 result = subprocess.run(rg_command, capture_output=True, text=True, check=False) 97 expired_strings = [] 98 if result.stdout.strip(): 99 for line in result.stdout.splitlines(): 100 match = re.search(r'<string name="([^"]+)"', line) 101 if match: 102 expired_strings.append(match.group(1)) 103 return expired_strings 104 105 106 def remove_expired_strings(expired_string_version): 107 """Remove expired strings in string.xml files using the original ripgrep.""" 108 rg_command = [ 109 "rg", 110 "-g", 111 "**/values/**", 112 "-l", 113 f'moz:removedIn="{expired_string_version}"', 114 MOBILE_ANDROID_DIR, 115 ] 116 result = subprocess.run(rg_command, capture_output=True, text=True, check=False) 117 if result.stdout.strip(): 118 files = result.stdout.strip().splitlines() 119 bash_command = ( 120 f"echo {' '.join(files)} | xargs perl -0777 -pi -e " 121 f'"s/(\\s*<!--(?:(?!<!--)[\\s\\S])*?-->\\s*)?<string[^>]*moz:removedIn=\\"{expired_string_version}\\"[^>]*>[^<]*<\\/string>//g"' 122 ) 123 subprocess.run(bash_command, shell=True, check=True, executable="/bin/bash") 124 return True 125 return False 126 127 128 def update_json_if_necessary(): 129 """Check if JSON files differ and copy if necessary.""" 130 if os.path.exists(SOURCE_JSON) and os.path.exists(TARGET_JSON): 131 result = subprocess.run(["cmp", "-s", SOURCE_JSON, TARGET_JSON], check=False) 132 if result.returncode != 0: # Files differ 133 subprocess.run(["cp", SOURCE_JSON, TARGET_JSON], check=True) 134 return True 135 return False 136 137 138 def search_remaining_occurrences(removed_strings): 139 """Search for remaining occurrences of each removed string.""" 140 remaining_use_message = "" 141 for name in removed_strings: 142 rg_command = [ 143 "rg", 144 "-n", 145 "--pcre2", 146 f"{name}(?![a-zA-Z0-9_-])", 147 MOBILE_ANDROID_DIR, 148 "-g", 149 "!**/strings.xml", 150 ] 151 result = subprocess.run(rg_command, capture_output=True, text=True, check=False) 152 if result.stdout.strip(): 153 lines = result.stdout.strip().splitlines() 154 remaining_use_message += ( 155 f"\n- \033[31m\033[1m{name}\033[0m ({len(lines)}):\n" 156 ) 157 for line in lines: 158 remaining_use_message += f"\t· {line}\n" 159 return remaining_use_message 160 161 162 def commit_changes(bug_id, new_version_number, strings_removed, json_updated): 163 """Commit all changes with a constructed commit message.""" 164 commit_message = ( 165 f"Bug {bug_id} - Start the nightly {new_version_number} development cycle.\n\n" 166 ) 167 if strings_removed: 168 commit_message += f"Strings expiring in version {new_version_number - EXPIRED_STRING_VERSION_OFFSET} have been removed\n" 169 if json_updated: 170 commit_message += ( 171 "search_telemetry_v2.json was updated in Android Components, based on the " 172 "content of services/settings/dumps/main/search-telemetry-v2.json\n" 173 ) 174 subprocess.run(["git", "add", "-u"], check=False) 175 subprocess.run(["git", "commit", "--quiet", "-m", commit_message], check=False) 176 177 178 def main(): 179 check_ripgrep_installed() 180 check_uncommitted_changes() 181 bug_id = get_bug_id() 182 previous_version = get_previous_version() 183 new_version = previous_version + 1 184 expired_string_version = new_version - EXPIRED_STRING_VERSION_OFFSET 185 186 # Update changelog 187 update_changelog(previous_version, new_version) 188 189 # Find and remove expired strings 190 expired_strings = find_expired_strings(expired_string_version) 191 if expired_strings: 192 strings_removed = remove_expired_strings(expired_string_version) 193 remaining_use_message = search_remaining_occurrences(expired_strings) 194 else: 195 strings_removed = False 196 remaining_use_message = "" 197 198 # Check JSON update 199 json_updated = update_json_if_necessary() 200 201 # Commit changes 202 commit_changes(bug_id, new_version, strings_removed, json_updated) 203 204 # Output final message 205 print(f"✅ Changelog updated to version {new_version}") 206 if strings_removed: 207 print(f"✅ Removed 'moz:removedIn=\"{expired_string_version}\"' entries") 208 else: 209 print(f"ℹ️ No 'moz:removedIn=\"{expired_string_version}\"' entries found") 210 211 if json_updated: 212 print("✅ search_telemetry_v2.json was updated in Android Components") 213 else: 214 print("ℹ️ search_telemetry_v2.json was already up to date and was not modified") 215 print(f"✅ Changes committed with Bug ID {bug_id}.") 216 217 if remaining_use_message: 218 print( 219 "\n⚠️ Some of the strings that were removed might still be used in the codebase." 220 ) 221 print( 222 "These are the potential remaining usages. Keep in mind that it is purely indicative and might show false positives." 223 ) 224 print("Please remove the real remaining usages and amend the commit.") 225 print(remaining_use_message) 226 227 print("\n\033[1mPlease make sure you complete the following steps:\033[0m") 228 if remaining_use_message: 229 print( 230 "☐ Remove the remaining uses of the removed strings and amend the commit." 231 ) 232 print("☐ Review the changes and make sure they are correct") 233 print("☐ Run `moz-phab submit --no-wip`") 234 print( 235 "☐ Run `mach try --preset firefox-android` and add a comment with the try link on the patch" 236 ) 237 238 239 if __name__ == "__main__": 240 main()