tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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()