tor-commits
Threads by month
- ----- 2026 -----
- January
- ----- 2025 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2024 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2023 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2022 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2021 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2020 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2019 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2018 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2017 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2016 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2015 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2014 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2013 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2012 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2011 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- 1 participants
- 214645 discussions
[Git][tpo/applications/tor-browser-build] Pushed new tag tbb-16.0a1-build1
by Pier Angelo Vendrame (@pierov) 11 Dec '25
by Pier Angelo Vendrame (@pierov) 11 Dec '25
11 Dec '25
Pier Angelo Vendrame pushed new tag tbb-16.0a1-build1 at The Tor Project / Applications / tor-browser-build
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser-build/-/tree/tbb…
You're receiving this email because of your account on gitlab.torproject.org.
1
0
[Git][tpo/applications/tor-browser-build][main] Bug 41512&41513: Prepare Tor&Mullvad Browser 16.0a1.
by Pier Angelo Vendrame (@pierov) 11 Dec '25
by Pier Angelo Vendrame (@pierov) 11 Dec '25
11 Dec '25
Pier Angelo Vendrame pushed to branch main at The Tor Project / Applications / tor-browser-build
Commits:
7be0ea5c by Pier Angelo Vendrame at 2025-12-11T16:00:27+01:00
Bug 41512&41513: Prepare Tor&Mullvad Browser 16.0a1.
- - - - -
9 changed files:
- projects/browser/Bundle-Data/Docs-MB/ChangeLog.txt
- projects/browser/Bundle-Data/Docs-TBB/ChangeLog.txt
- projects/browser/config
- projects/firefox/config
- projects/geckoview/config
- projects/go/config
- projects/moat-settings/config
- projects/translation/config
- rbm.conf
Changes:
=====================================
projects/browser/Bundle-Data/Docs-MB/ChangeLog.txt
=====================================
@@ -1,3 +1,40 @@
+Mullvad Browser 16.0a1 - December 11 2025
+ * All Platforms
+ * Updated Firefox to 146.0a1esr
+ * Updated NoScript to 13.5.1.90101984
+ * Updated Mullvad Browser Extension to 0.9.7
+ * Updated uBlock Origin to 1.68.0
+ * Bug 483: Add the "No AI" version of DuckDuckGo to available search engines [mullvad-browser]
+ * Bug 487: Search engines are sorted alphabetically rather than the desired order [mullvad-browser]
+ * Bug 496: Rebase Mullvad Browser Alpha onto 146.0a1 [mullvad-browser]
+ * Bug 22974: Self-host NoScript Updates [tor-browser]
+ * Bug 44118: Decide how to deal with upstream feature gates in a RR world [tor-browser]
+ * Bug 44302: Fix the flags of some about pages [tor-browser]
+ * Bug 44310: Default zoom always resets to 100% [tor-browser]
+ * Bug 44333: Add the "No AI" version of DuckDuckGo to available search engines [tor-browser]
+ * Bug 44365: Fix CSS linting errors introduced for firefox 145 [tor-browser]
+ * Bug 44391: Restrictions cascade blocks every capability in subframes (e.g. captchas) [tor-browser]
+ * Windows
+ * Bug 479: Fix WebRTC on mingw after the 144 rebase [mullvad-browser]
+ * Bug 500: Restore NSIS plugin Makefiles [mullvad-browser]
+ * Linux
+ * Bug 434: Make Linux aarch64 builds available through the Alpha channel [mullvad-browser]
+ * Bug 44273: Restore Noto CJK as Jigmo has a too low readability [tor-browser]
+ * Bug 44315: Font/text issue with self-upgrade window [tor-browser]
+ * Build System
+ * All Platforms
+ * Bug 493: Automatically check whether the signed build is avaliable on dist.torproject.org before notifying Mullvad for QA [mullvad-browser]
+ * Bug 40903: Make xz multi-threaded [tor-browser-build]
+ * Bug 41643: Update toolchains for Firefox 146 [tor-browser-build]
+ * Bug 41644: Self-hosted browser extensions support in relprep.py [tor-browser-build]
+ * Bug 41647: Add a XZ_DEFAULTS to set_default_env [tor-browser-build]
+ * Bug 41662: Add python-zstandard to desktop containers [tor-browser-build]
+ * Linux
+ * Bug 41627: Firefox build system needs a more recent version of OpenSSL on Linux [tor-browser-build]
+ * Bug 41632: Ship Linux aarch64 builds in Alpha Release Channel [tor-browser-build]
+ * Bug 41639: Remove linux-arm target [tor-browser-build]
+ * Bug 41640: Clean the GCC config file [tor-browser-build]
+
Mullvad Browser 15.0a4 - October 16 2025
* All Platforms
* Updated Firefox to 140.4.0esr
=====================================
projects/browser/Bundle-Data/Docs-TBB/ChangeLog.txt
=====================================
@@ -1,3 +1,66 @@
+Tor Browser 16.0a1 - December 11 2025
+ * All Platforms
+ * Updated NoScript to 13.5.1.90101984
+ * Bug 43792: Determine whether noscript_persist users need to have their NoScript settings migrated [tor-browser]
+ * Bug 43998: Rebase Tor Browser Alpha onto 141.0 [tor-browser]
+ * Bug 44118: Decide how to deal with upstream feature gates in a RR world [tor-browser]
+ * Bug 44280: Test stream isolation [tor-browser]
+ * Bug 44333: Add the "No AI" version of DuckDuckGo to available search engines [tor-browser]
+ * Bug 44365: Fix CSS linting errors introduced for firefox 145 [tor-browser]
+ * Bug 44391: Restrictions cascade blocks every capability in subframes (e.g. captchas) [tor-browser]
+ * Bug 44408: Fix TorDomainIsolator in 146 [tor-browser]
+ * Bug 44418: Update built-in obfs4 bridges [tor-browser]
+ * Bug 41609: Move back to CDN77 for default snowflake bridge [tor-browser-build]
+ * Bug 41646: Update lyrebird version to v0.7.0 [tor-browser-build]
+ * Bug 483: Add the "No AI" version of DuckDuckGo to available search engines [mullvad-browser]
+ * Bug 487: Search engines are sorted alphabetically rather than the desired order [mullvad-browser]
+ * Windows + macOS + Linux
+ * Updated Firefox to 146.0a1esr
+ * Bug 44302: Fix the flags of some about pages [tor-browser]
+ * Bug 44310: Default zoom always resets to 100% [tor-browser]
+ * Bug 44411: DownloadsTorWarning.sys.mjs fails to load [tor-browser]
+ * Bug 44419: Re-add the moz-toggle title attribute patch [tor-browser]
+ * Linux
+ * Bug 44273: Restore Noto CJK as Jigmo has a too low readability [tor-browser]
+ * Bug 44315: Font/text issue with self-upgrade window [tor-browser]
+ * Android
+ * Updated GeckoView to 146.0a1esr
+ * Bug 44303: Extension update job might never work on Android [tor-browser]
+ * Bug 44324: Refactor the NSS no proxy-bypass patch on A-S to avoid -Wunreachable-code [tor-browser]
+ * Build System
+ * All Platforms
+ * Bug 43951: Include product with the component in triage CSVs [tor-browser]
+ * Bug 40903: Make xz multi-threaded [tor-browser-build]
+ * Bug 41445: Better handle bugzilla "Web Compatibility" issues in generate-bugzilla-triage-csv [tor-browser-build]
+ * Bug 41607: Fix regression from #41373 in tools/count-mar-downloads [tor-browser-build]
+ * Bug 41608: Add per-platform numbers for incrementals in count-mar-downloads [tor-browser-build]
+ * Bug 41643: Update toolchains for Firefox 146 [tor-browser-build]
+ * Bug 41644: Self-hosted browser extensions support in relprep.py [tor-browser-build]
+ * Bug 41647: Add a XZ_DEFAULTS to set_default_env [tor-browser-build]
+ * Bug 41648: Add a mozconfig to projects/geckoview [tor-browser-build]
+ * Bug 41654: Make relprep.py update Moat settings [tor-browser-build]
+ * Windows + macOS + Linux
+ * Bug 41662: Add python-zstandard to desktop containers [tor-browser-build]
+ * Windows + Linux + Android
+ * Bug 41580: Update Go major to 1.25.x [tor-browser-build]
+ * Updated Go to 1.25.5
+ * Linux
+ * Bug 41601: Upstream droppped Linux i686 support [tor-browser-build]
+ * Bug 41627: Firefox build system needs a more recent version of OpenSSL on Linux [tor-browser-build]
+ * Bug 41632: Ship Linux aarch64 builds in Alpha Release Channel [tor-browser-build]
+ * Bug 41639: Remove linux-arm target [tor-browser-build]
+ * Bug 41640: Clean the GCC config file [tor-browser-build]
+ * Android
+ * Bug 44360: Patch the vendored Gradle Nimbus plugin [tor-browser]
+ * Bug 44370: Always produce target.maven.zip [tor-browser]
+ * Bug 41485: Retire get_gradle_dependencies_list and create a simple way to update gradle deps [tor-browser-build]
+ * Bug 41573: Remove Android x86 support [tor-browser-build]
+ * Bug 41617: Pass page size to zipalign [tor-browser-build]
+ * Bug 41620: Do not rerun zipalign when signing [tor-browser-build]
+ * Bug 41621: Remove support using older android build tools when signing 14.5 releases in tools/signing/wrappers/sign-apk [tor-browser-build]
+ * Bug 41628: android_ndk_version and revision can be grouped together [tor-browser-build]
+ * Bug 41636: Update the scripts to manage Gradle dependencies [tor-browser-build]
+
Tor Browser 15.0a4 - October 16 2025
* All Platforms
* Updated NoScript to 13.2.1
=====================================
projects/browser/config
=====================================
@@ -108,16 +108,16 @@ input_files:
enable: '[% ! c("var/android") %]'
- filename: dmg-root
enable: '[% ! c("var/android") %]'
- - URL: https://addons.mozilla.org/firefox/downloads/file/4593796/noscript-13.2.1.x…
+ - URL: https://dist.torproject.org/torbrowser/noscript/noscript-13.5.1.90101984.xpi
name: noscript
- sha256sum: 190297f3d1e55db0c65f9bc00460bea9b753939d428ea593d6cef27fde1ce69a
- - URL: https://addons.mozilla.org/firefox/downloads/file/4578681/ublock_origin-1.6…
+ sha256sum: 4731db06df1e433f843cd0d3d449241c9f143ff9d5bbf787c8162bfa63c8ab6c
+ - URL: https://addons.mozilla.org/firefox/downloads/file/4629131/ublock_origin-1.6…
name: ublock-origin
- sha256sum: bc62cd930601212f1568964389352bbd4b1808466f2c9ac1198c754338077fb0
+ sha256sum: 5caf4abda494018841222a12156919bbdd8cad82a783c38c36b22dd642704315
enable: '[% c("var/mullvad-browser") %]'
- - URL: https://cdn.mullvad.net/browser-extension/0.9.5/mullvad-browser-extension-0…
+ - URL: https://cdn.mullvad.net/browser-extension/0.9.7/mullvad-browser-extension-0…
name: mullvad-extension
- sha256sum: c36f8d5447300621579f7f1408a25f25dac3929c867e7ae5db9fc8b82e80a4e9
+ sha256sum: 4597ee6fff6a2f19cbf78a6149d38d12ad8a9d6029b7e36f6c639dc3e07dd2c3
enable: '[% c("var/mullvad-browser") %]'
- filename: 'gtk3-settings.ini'
enable: '[% c("var/linux") %]'
=====================================
projects/firefox/config
=====================================
@@ -19,9 +19,9 @@ var:
firefox_platform_version: '146.0a1'
firefox_version: '[% c("var/firefox_platform_version") %]'
browser_series: '16.0'
- browser_rebase: 1
+ browser_rebase: 2
browser_branch: '[% c("var/browser_series") %]-[% c("var/browser_rebase") %]'
- browser_build: 1
+ browser_build: 2
copyright_year: '[% exec("git show -s --format=%ci " _ c("git_hash") _ "^{commit}", { exec_noco => 1 }).remove("-.*") %]'
nightly_updates_publish_dir: '[% c("var/nightly_updates_publish_dir_prefix") %]nightly-[% c("var/osname") %]'
gitlab_project: https://gitlab.torproject.org/tpo/applications/tor-browser
=====================================
projects/geckoview/config
=====================================
@@ -21,9 +21,9 @@ var:
firefox_platform_version: '146.0a1'
geckoview_version: '[% c("var/firefox_platform_version") %]'
browser_series: '16.0'
- browser_rebase: 1
+ browser_rebase: 2
browser_branch: '[% c("var/browser_series") %]-[% c("var/browser_rebase") %]'
- browser_build: 1
+ browser_build: 2
gitlab_project: https://gitlab.torproject.org/tpo/applications/tor-browser
git_commit: '[% exec("git rev-parse " _ c("git_hash") _ "^{commit}", { exec_noco => 1 }) %]'
deps:
=====================================
projects/go/config
=====================================
@@ -1,11 +1,11 @@
# vim: filetype=yaml sw=2
-version: '1.25.3'
+version: '1.25.5'
filename: '[% project %]-[% c("version") %]-[% c("var/osname") %]-[% c("var/build_id") %].tar.[% c("compress_tar") %]'
container:
use_container: 1
var:
- source_sha256: a81a4ba593d0015e10c51e267de3ff07c7ac914dfca037d9517d029517097795
+ source_sha256: 22a5fd0a91efcd28a1b0537106b9959b2804b61f59c3758b51e8e5429c1a954f
no_crosscompile: 1
setup: |
mkdir -p /var/tmp/dist
=====================================
projects/moat-settings/config
=====================================
@@ -1,7 +1,7 @@
# vim: filetype=yaml sw=2
filename: '[% project %]-[% c("version") %]-[% c("var/build_id") %].tar.[% c("compress_tar") %]'
git_url: https://gitlab.torproject.org/tpo/anti-censorship/rdsys-admin.git
-git_hash: 810fb24bd5fe36c8c0a67ecf8f8ec47b479eee31
+git_hash: c13a8e5909d78abbabf5b853e20e63e38fcc4808
version: '[% c("abbrev") %]'
# Use gz for now, since we do not support Zstandard on Linux containers.
compress_tar: 'gz'
=====================================
projects/translation/config
=====================================
@@ -12,13 +12,13 @@ compress_tar: 'gz'
steps:
base-browser:
base-browser: '[% INCLUDE build %]'
- git_hash: dff70d135408cfc24931c170efa91fbaded19914
+ git_hash: d366318349af0977ff23b492fa87e436e8672fdd
targets:
nightly:
git_hash: 'base-browser'
tor-browser:
tor-browser: '[% INCLUDE build %]'
- git_hash: ca310e42296a7085ea59fc323592f3dc702123ac
+ git_hash: 45b67d3be52f79e8a4b361fd04ab12f342ab0016
targets:
nightly:
git_hash: 'tor-browser'
@@ -32,7 +32,7 @@ steps:
fenix: '[% INCLUDE build %]'
# We need to bump the commit before releasing but just pointing to a branch
# might cause too much rebuidling of the Firefox part.
- git_hash: 0efa38c746df88feeb4daa7150e394e87e6d4d18
+ git_hash: 4690b13484b82453db1482af32953ee2c9398568
compress_tar: 'zst'
targets:
nightly:
=====================================
rbm.conf
=====================================
@@ -81,11 +81,11 @@ buildconf:
git_signtag_opt: '-s'
var:
- torbrowser_version: '15.0a4'
+ torbrowser_version: '16.0a1'
torbrowser_build: 'build1'
# This should be the date of when the build is started. For the build
# to be reproducible, browser_release_date should always be in the past.
- browser_release_date: '2025/10/15 18:00:00'
+ browser_release_date: '2025/12/11 14:20:12'
browser_release_date_timestamp: '[% USE date; date.format(c("var/browser_release_date"), "%s") %]'
browser_default_channel: alpha
browser_platforms:
@@ -131,9 +131,9 @@ var:
updater_enabled: 1
build_mar: 1
torbrowser_incremental_from:
+ - 15.0a4
- 15.0a3
- 15.0a2
- - 15.0a1
mar_channel_id: '[% c("var/projectname") %]-torproject-[% c("var/channel") %]'
# torbrowser_legacy_version: 13.5.22
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser-build/-/commit/7…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser-build/-/commit/7…
You're receiving this email because of your account on gitlab.torproject.org.
1
0
[Git][tpo/applications/mullvad-browser][mullvad-browser-146.0a1-16.0-2] 7 commits: fixup! BB 41803: Add some developer tools for working on tor-browser.
by henry (@henry) 11 Dec '25
by henry (@henry) 11 Dec '25
11 Dec '25
henry pushed to branch mullvad-browser-146.0a1-16.0-2 at The Tor Project / Applications / Mullvad Browser
Commits:
afe3ec76 by Henry Wilkes at 2025-12-11T14:51:14+00:00
fixup! BB 41803: Add some developer tools for working on tor-browser.
TB 44367: Make git_get return the stdout string, rather than a list.
Add git_lines to generate lines.
- - - - -
e7ee2c3c by Henry Wilkes at 2025-12-11T14:51:15+00:00
fixup! BB 41803: Add some developer tools for working on tor-browser.
TB 44367: Use raw diff to get list of file changes.
- - - - -
e33552a3 by Henry Wilkes at 2025-12-11T14:51:16+00:00
fixup! BB 41803: Add some developer tools for working on tor-browser.
TB 44367: Add type annotations and parameter documentation.
- - - - -
d2f95e32 by Henry Wilkes at 2025-12-11T14:51:17+00:00
fixup! BB 41803: Add some developer tools for working on tor-browser.
TB 44367: Make the argcomplete module optional.
- - - - -
d40254c1 by Henry Wilkes at 2025-12-11T14:51:18+00:00
fixup! BB 41803: Add some developer tools for working on tor-browser.
TB 44367: Use function caching instead of global variables.
- - - - -
984a9ef9 by Henry Wilkes at 2025-12-11T14:51:19+00:00
fixup! BB 41803: Add some developer tools for working on tor-browser.
TB 44367: Fetch FIREFOX_ tags from the remote if they are missing.
- - - - -
07370c67 by Henry Wilkes at 2025-12-11T14:51:20+00:00
fixup! BB 41803: Add some developer tools for working on tor-browser.
TB 44367: Improve the auto-fixup/auto-commit command.
The auto-fixup command was renamed to auto-commit. It now also handles:
1. Already staged changes.
2. Untracked/added files.
3. Removed files.
4. Renamed files.
5. Allowing the user to create a new commit.
- - - - -
1 changed file:
- tools/base_browser/tb-dev
Changes:
=====================================
tools/base_browser/tb-dev
=====================================
@@ -6,6 +6,7 @@ Useful tools for working on tor-browser repository.
import argparse
import atexit
+import functools
import json
import os
import re
@@ -14,8 +15,15 @@ import sys
import tempfile
import termios
import urllib.request
+from collections.abc import Callable, Iterable, Iterator
+from types import ModuleType
+from typing import Any, NotRequired, TypedDict, TypeVar
-import argcomplete
+argcomplete: None | ModuleType = None
+try:
+ import argcomplete
+except ImportError:
+ pass
GIT_PATH = "/usr/bin/git"
UPSTREAM_URLS = {
@@ -36,9 +44,14 @@ class TbDevException(Exception):
pass
-def git_run(args, check=True, env=None):
+def git_run(
+ args: list[str], check: bool = True, env: None | dict[str, str] = None
+) -> None:
"""
Run a git command with output sent to stdout.
+ :param args: The arguments to pass to git.
+ :param check: Whether to check for success.
+ :param env: Optional environment to set.
"""
if env is not None:
tmp_env = dict(os.environ)
@@ -51,46 +64,122 @@ def git_run(args, check=True, env=None):
raise TbDevException(str(err)) from err
-def git_get(args):
+def git_run_pager(
+ args: list[str] | None = None,
+ arg_sequence: Iterable[list[str]] | None = None,
+ pager_prefix: None | str = None,
+) -> None:
"""
- Run a git command with each non-empty line returned in a list.
+ Run a sequence of git commands with the output concatenated and sent to the
+ git pager.
+ :param args: The arguments to pass to git, or `None` if a sequence is desired.
+ :param arg_sequence: A sequence representing several git commands.
+ :param pager_prefix: An optional text to send to the pager first.
+ """
+ if arg_sequence is None:
+ if args is not None:
+ arg_sequence = (args,)
+ else:
+ raise ValueError("Missing `arg_sequence` or `args`")
+ elif args is not None:
+ raise ValueError("Unexpected both args and arg_sequence")
+
+ pager = git_get(["var", "GIT_PAGER"])
+ if not pager:
+ raise TbDevException("Missing a GIT_PAGER")
+ command = [pager]
+ if os.path.basename(pager) == "less":
+ # Show colours.
+ command.append("-R")
+
+ pager_process = subprocess.Popen(command, stdin=subprocess.PIPE, text=True)
+ assert pager_process.stdin is not None
+
+ if pager_prefix is not None:
+ pager_process.stdin.write(pager_prefix)
+ pager_process.stdin.flush()
+
+ for git_args in arg_sequence:
+ subprocess.run(
+ [GIT_PATH, "--no-pager", *git_args], check=False, stdout=pager_process.stdin
+ )
+
+ pager_process.stdin.close()
+
+ status = pager_process.wait()
+ if status != 0:
+ raise TbDevException(f"git pager {pager} exited with status {status}")
+
+
+def git_get(args: list[str], strip: bool = True, check: bool = True) -> str:
+ """
+ Return the output from a git command.
+ :param args: The arguments to send to git.
+ :param strip: Whether to strip the whitespace from the output.
+ :param check: Whether to check for success.
+ :returns: The stdout.
"""
try:
git_process = subprocess.run(
- [GIT_PATH, *args], text=True, stdout=subprocess.PIPE, check=True
+ [GIT_PATH, *args], text=True, stdout=subprocess.PIPE, check=check
)
except subprocess.CalledProcessError as err:
raise TbDevException(str(err)) from err
- return [line for line in git_process.stdout.split("\n") if line]
+ ret = git_process.stdout
+ if strip:
+ ret = ret.strip()
+ return ret
-local_root = None
+def git_lines(args: list[str]) -> Iterator[str]:
+ """
+ Yields the non-empty lines returned by the git command.
+ :param args: The arguments to send to git.
+ :yield: The lines.
+ """
+ for line in git_get(args, strip=False).split("\n"):
+ if not line:
+ continue
+ yield line
+
+
+def git_path_args(path_iter: Iterable[str]) -> Iterator[str]:
+ """
+ Generate the trailing arguments to specify paths in git commands, includes
+ the "--" separator just before the paths.
+ :param path_iter: The paths that should be passed in.
+ :yields: The git arguments.
+ """
+ yield "--"
+ for path in path_iter:
+ yield f":(literal){path}"
-def get_local_root():
+(a)functools.cache
+def get_local_root() -> str:
"""
Get the path for the tor-browser root directory.
+ :returns: The local root.
"""
- global local_root
- if local_root is None:
- try:
- # Make sure we have a matching remote in this git repository.
- if get_upstream_details()["is-browser-repo"]:
- local_root = git_get(["rev-parse", "--show-toplevel"])[0]
- else:
- local_root = ""
- except TbDevException:
- local_root = ""
- return local_root
+ try:
+ # Make sure we have a matching remote in this git repository.
+ if get_upstream_details()["is-browser-repo"] == "True":
+ return git_get(["rev-parse", "--show-toplevel"])
+ else:
+ return ""
+ except TbDevException:
+ return ""
-def determine_upstream_details():
+(a)functools.cache
+def get_upstream_details() -> dict[str, str]:
"""
- Determine details about the upstream.
+ Get details about the upstream repository.
+ :returns: The details.
"""
remote_urls = {
- remote: git_get(["remote", "get-url", remote])[0]
- for remote in git_get(["remote"])
+ remote: git_get(["remote", "get-url", remote])
+ for remote in git_lines(["remote"])
}
matches = {
@@ -102,7 +191,7 @@ def determine_upstream_details():
}
is_browser_repo = len(matches) > 0
- details = {"is-browser-repo": is_browser_repo}
+ details = {"is-browser-repo": str(is_browser_repo)}
origin_remote_repo = matches.get("origin", None)
upstream_remote_repo = matches.get("upstream", None)
@@ -125,31 +214,30 @@ def determine_upstream_details():
return details
-cached_upstream_details = None
-
-
-def get_upstream_details():
- """
- Get details about the upstream repository.
- """
- global cached_upstream_details
- if cached_upstream_details is None:
- cached_upstream_details = determine_upstream_details()
- return cached_upstream_details
-
-
class Reference:
"""Represents a git reference to a commit."""
- def __init__(self, name, commit):
- self.name = name
+ _REFS_REGEX = re.compile(r"refs/[a-z]+/")
+
+ def __init__(self, full_name: str, commit: str) -> None:
+ """
+ :param full_name: The full reference name. E.g. "refs/tags/MyTag".
+ :param commit: The commit hash for the commit this reference points to.
+ """
+ match = self.__class__._REFS_REGEX.match(full_name)
+ if not match:
+ raise ValueError(f"Invalid reference name {full_name}")
+ self.full_name = full_name
+ self.name = full_name[match.end() :]
self.commit = commit
-def get_refs(ref_type, name_start):
+def get_refs(ref_type: str, name_start: str) -> Iterator[Reference]:
"""
- Get a list of references that match the given 'ref_type' ("tag" or "remote"
- or "head") that starts with the given 'name_start'.
+ Get a list of references that match the given conditions.
+ :param ref_type: The ref type to search for ("tag" or "remote" or "head").
+ :param name_start: The ref name start to match against.
+ :yield: The matching references.
"""
if ref_type == "tag":
ref_start = "refs/tags/"
@@ -163,56 +251,83 @@ def get_refs(ref_type, name_start):
fstring = "%(*objectname),%(objectname),%(refname)"
pattern = f"{ref_start}{name_start}**"
- def line_to_ref(line):
+ def line_to_ref(line: str) -> Reference:
[objectname_reference, objectname, ref_name] = line.split(",", 2)
# For annotated tags, the objectname_reference is non-empty and points
# to an actual commit.
# For remotes, heads and lightweight tags, the objectname_reference will
# be empty and objectname will point directly to the commit.
- return Reference(
- ref_name.replace(ref_start, "", 1), objectname_reference or objectname
- )
+ return Reference(ref_name, objectname_reference or objectname)
- return [
+ return (
line_to_ref(line)
- for line in git_get(["for-each-ref", f"--format={fstring}", pattern])
- ]
+ for line in git_lines(["for-each-ref", f"--format={fstring}", pattern])
+ )
-def get_nearest_ref(ref_type, name_start, search_from):
+def get_firefox_ref(search_from: str) -> Reference:
"""
- Search backwards from the 'search_from' commit to find the first commit
- that matches the given 'ref_type' that starts with the given 'name_start'.
+ Search for the commit that comes from firefox.
+ :param search_from: The commit to search backwards from.
+ :returns: The firefox reference.
"""
- ref_list = get_refs(ref_type, name_start)
+ # Only search a limited history that should include the FIREFOX_ tag.
+ search_commits = [c for c in git_lines(["rev-list", "-1000", search_from])]
+
+ firefox_tag_prefix = "FIREFOX_"
- for commit in git_get(["rev-list", "-1000", search_from]):
- for ref in ref_list:
+ existing_tags = list(get_refs("tag", firefox_tag_prefix))
+ for commit in search_commits:
+ for ref in existing_tags:
if commit == ref.commit:
return ref
- raise TbDevException(f"No {name_start} commit found in the last 1000 commits")
-
-
-def get_firefox_ref(search_from):
+ # Might just need to fetch tags from the remote.
+ upstream = get_upstream_details().get("remote", None)
+ if upstream:
+ remote_ref: None | Reference = None
+ search_index = len(search_commits)
+ # Search the remote for a tag that is in our history.
+ # We want to avoid triggering a long fetch, so we just want to grab the
+ # tag that already points to a commit in our history.
+ for line in git_lines(
+ ["ls-remote", upstream, f"refs/tags/{firefox_tag_prefix}*"]
+ ):
+ objectname, name = line.split("\t", 1)
+ for index in range(search_index):
+ if search_commits[index] == objectname:
+ # Remove trailing "^{}" for commits pointed to by
+ # annotated tags.
+ remote_ref = Reference(re.sub(r"\^\{\}$", "", name), objectname)
+ # Only continue to search for references that are even
+ # closer to `search_from`.
+ search_index = index
+ break
+ if remote_ref is not None:
+ # Get a local copy of just this tag.
+ git_run(["fetch", "--no-tags", upstream, "tag", remote_ref.name])
+ return ref
+
+ raise TbDevException("Unable to find FIREFOX_ tag")
+
+
+def get_upstream_tracking_branch(search_from: str) -> str:
"""
- Search backwards from the 'search_from' commit to find the commit that comes
- from firefox.
+ :param search_from: The commit reference.
+ :returns: The upstream branch reference name.
"""
- return get_nearest_ref("tag", "FIREFOX_", search_from)
-
-
-def get_upstream_tracking_branch(search_from):
- return git_get(["rev-parse", "--abbrev-ref", f"{search_from}@{{upstream}}"])[0]
+ return git_get(["rev-parse", "--abbrev-ref", f"{search_from}@{{upstream}}"])
-def get_upstream_basis_commit(search_from):
+def get_upstream_basis_commit(search_from: str) -> str:
"""
Get the first common ancestor of search_from that is also in its upstream
branch.
+ :param search_from: The commit reference.
+ :returns: The upstream commit hash.
"""
upstream_branch = get_upstream_tracking_branch(search_from)
- commit = git_get(["merge-base", search_from, upstream_branch])[0]
+ commit = git_get(["merge-base", search_from, upstream_branch])
# Verify that the upstream commit shares the same firefox basis. Otherwise,
# this would indicate that the upstream is on an early or later FIREFOX
# base.
@@ -226,26 +341,82 @@ def get_upstream_basis_commit(search_from):
return commit
-def get_changed_files(from_commit, staged=False):
+class FileChange:
+ """Represents a git change to a commit."""
+
+ def __init__(self, status: str, path: str, new_path: str) -> None:
+ """
+ :param status: The file change status used within git diff. E.g. "M" for
+ modified, or "D" for deleted.
+ :param path: The source file path.
+ :param new_path: The file path after the change.
+ """
+ self.status = status
+ self.path = path
+ self.new_path = new_path
+
+
+RAW_DIFF_PATH_PATTERN = r"(?P<path>[^\0]*)\0"
+RAW_DIFF_LINE_REGEX = re.compile(
+ r":[0-7]+ [0-7]+ [0-9a-f]+ [0-9a-f]+ (?P<status>[ADMTUXRC])[0-9]*\0"
+ + RAW_DIFF_PATH_PATTERN
+)
+RAW_DIFF_PATH_REGEX = re.compile(RAW_DIFF_PATH_PATTERN)
+
+
+def parse_raw_diff_line(raw_output: str) -> tuple[FileChange, int]:
"""
- Get a list of filenames relative to the current working directory that have
+ Parse the --raw diff output from git.
+ :param raw_output: The raw output.
+ :returns: The change for this line, and the offset for the end of the raw
+ diff line.
+ """
+ match = RAW_DIFF_LINE_REGEX.match(raw_output)
+ if not match:
+ raise ValueError(f"Invalid raw output: {raw_output[:50]}...")
+ path = os.path.relpath(os.path.join(get_local_root(), match.group("path")))
+ status = match.group("status")
+ if status in ("R", "C"):
+ match = RAW_DIFF_PATH_REGEX.match(raw_output, pos=match.end())
+ if not match:
+ raise ValueError(f"Invalid raw output for rename: {raw_output[:50]}...")
+ new_path = os.path.relpath(os.path.join(get_local_root(), match.group("path")))
+ else:
+ new_path = path
+
+ return FileChange(status, path, new_path), match.end()
+
+
+def get_changed_files(
+ from_commit: None | str = None, staged: bool = False
+) -> Iterator[FileChange]:
+ """
+ Get a list of file changes relative to the current working directory that have
been changed since 'from_commit' (non-inclusive).
+ :param from_commit: The commit to compare against, otherwise use the git
+ diff default.
+ :param staged: Whether to limit the diff to staged changes.
+ :yield: The file changes.
"""
- args = ["diff"]
+ args = ["diff", "-z", "--raw"]
if staged:
args.append("--staged")
- args.append("--name-only")
- args.append(from_commit)
- return [
- os.path.relpath(os.path.join(get_local_root(), filename))
- for filename in git_get(args)
- ]
+ if from_commit:
+ args.append(from_commit)
+ raw_output = git_get(args, strip=False)
+ while raw_output:
+ file_change, end = parse_raw_diff_line(raw_output)
+ yield file_change
+ raw_output = raw_output[end:]
-def file_contains(filename, regex):
+def file_contains(filename: str, regex: re.Pattern[str]) -> bool:
"""
Return whether the file is a utf-8 text file containing the regular
expression given by 'regex'.
+ :param filename: The file path.
+ :param regex: The pattern to search for.
+ :returns: Whether the pattern was matched.
"""
with open(filename, encoding="utf-8") as file:
try:
@@ -258,9 +429,10 @@ def file_contains(filename, regex):
return False
-def get_gitlab_default():
+def get_gitlab_default() -> str:
"""
Get the name of the default branch on gitlab.
+ :returns: The branch name.
"""
repo_name = get_upstream_details().get("repo-name", None)
if repo_name is None:
@@ -283,12 +455,14 @@ def get_gitlab_default():
)
with urllib.request.urlopen(gitlab_request, timeout=20) as response:
- return json.load(response)["data"]["project"]["repository"]["rootRef"]
+ default = json.load(response)["data"]["project"]["repository"]["rootRef"]
+ assert isinstance(default, str)
+ return default
-def within_browser_root():
+def within_browser_root() -> bool:
"""
- Whether we are with the tor browser root.
+ :returns: Whether we are with the tor browser root.
"""
root = get_local_root()
if not root:
@@ -301,24 +475,24 @@ def within_browser_root():
# * -------------------- *
-def show_firefox_commit(_args):
+def show_firefox_commit(_args: argparse.Namespace) -> None:
"""
Print the tag name and commit for the last firefox commit below the current
HEAD.
"""
ref = get_firefox_ref("HEAD")
- print(ref.name)
+ print(ref.full_name)
print(ref.commit)
-def show_upstream_basis_commit(_args):
+def show_upstream_basis_commit(_args: argparse.Namespace) -> None:
"""
Print the last upstream commit for the current HEAD.
"""
print(get_upstream_basis_commit("HEAD"))
-def show_log(args):
+def show_log(args: argparse.Namespace) -> None:
"""
Show the git log between the current HEAD and the last firefox commit.
"""
@@ -326,7 +500,7 @@ def show_log(args):
git_run(["log", f"{commit}..HEAD", *args.gitargs], check=False)
-def show_files_containing(args):
+def show_files_containing(args: argparse.Namespace) -> None:
"""
List all the files that that have been modified for tor browser, that also
contain a regular expression.
@@ -336,33 +510,32 @@ def show_files_containing(args):
except re.error as err:
raise TbDevException(f"{args.regex} is not a valid python regex") from err
- file_list = get_changed_files(get_firefox_ref("HEAD").commit)
-
- for filename in file_list:
- if not os.path.isfile(filename):
+ for file_change in get_changed_files(get_firefox_ref("HEAD").commit):
+ path = file_change.new_path
+ if not os.path.isfile(path):
# deleted ofile
continue
- if file_contains(filename, regex):
- print(filename)
+ if file_contains(path, regex):
+ print(path)
-def show_changed_files(_args):
+def show_changed_files(_args: argparse.Namespace) -> None:
"""
List all the files that have been modified relative to upstream.
"""
- for filename in get_changed_files(get_upstream_basis_commit("HEAD")):
- print(filename)
+ for file_change in get_changed_files(get_upstream_basis_commit("HEAD")):
+ print(file_change.new_path)
-def lint_changed_files(args):
+def lint_changed_files(args: argparse.Namespace) -> None:
"""
Lint all the files that have been modified relative to upstream.
"""
os.chdir(get_local_root())
file_list = [
- f
+ f.new_path
for f in get_changed_files(get_upstream_basis_commit("HEAD"))
- if os.path.isfile(f) # Not deleted
+ if os.path.isfile(f.new_path) # Not deleted
]
# We add --warnings since clang only reports whitespace issues as warnings.
subprocess.run(
@@ -371,10 +544,18 @@ def lint_changed_files(args):
)
-def prompt_user(prompt, convert):
+# TODO: replace with "prompt_user[T](..., T]) -> T" after python 3.12 is the
+# minimum mach version.
+T = TypeVar("T")
+
+
+def prompt_user(prompt: str, convert: Callable[[str], T]) -> T:
"""
- Ask the user for some input until the given converter returns without
- throwing a ValueError.
+ Ask the user for some input.
+ :param prompt: The prompt to show the user.
+ :param convert: A method to convert the response into a type. Should
+ throw `ValueError` if the user should be re-prompted for a valid input.
+ :returns: The first valid user response.
"""
while True:
# Flush out stdin.
@@ -388,8 +569,12 @@ def prompt_user(prompt, convert):
pass
-def binary_reply_default_no(value):
- """Process a 'y' or 'n' reply, defaulting to 'n' if empty."""
+def binary_reply_default_no(value: str) -> bool:
+ """
+ Process a 'y' or 'n' reply, defaulting to 'n' if empty.
+ :param value: The user input.
+ :returns: Whether the answer is yes.
+ """
if value == "":
return False
if value.lower() == "y":
@@ -399,121 +584,737 @@ def binary_reply_default_no(value):
raise ValueError()
-def get_fixup_for_file(filename, firefox_commit):
- """Find the commit the given file should fix up."""
+class FixupTarget:
+ """Represents a commit that can be targeted by a fixup."""
+
+ def __init__(self, commit: str, short_ref: str, title: str) -> None:
+ """
+ :param commit: The commit hash for the commit.
+ :param short_ref: The shortened commit hash for display.
+ :param title: The first line of the commit message.
+ """
+ self.commit = commit
+ self.short_ref = short_ref
+ self.title = title
+ self.changes: list[FileChange] = []
+ self.fixups: list[FixupTarget] = []
+ self.target: None | FixupTarget = None
+
+ _FIXUP_REGEX = re.compile(r"^fixup! +")
+
+ def trim_fixup(self) -> tuple[str, int]:
+ """
+ Trim the "fixup!" prefixes.
+ :returns: The stripped commit title and the fixup depth (how many fixups
+ prefixes there were).
+ """
+ title = self.title
+ depth = 0
+ while True:
+ match = self.__class__._FIXUP_REGEX.match(title)
+ if not match:
+ return title, depth
+ title = title[match.end() :]
+ depth += 1
+
+ def touches_path(
+ self, path: str, filter_status: None | str = None, check_dir: bool = False
+ ) -> bool:
+ """
+ Whether this target, or one of its fixups or target, touches the given
+ path.
+ :param path: The path to check.
+ :param filter_status: Limit the detected changes to the given status(es).
+ :param check_dir: Whether we should treat `path` as a directory and check for
+ files within it.
+ :returns: Whether this target matches.
+ """
+ # NOTE: In the case of renames, we generally assume that renames occur
+ # in the fixup targets. E.g. "Commit 1" creates the file "file.txt", and
+ # "fixup! Commit 1" renames it to "new.txt". In this case, if the
+ # FixupTarget for "Commit 1" is passed in "file.txt" it will match. And
+ # if it is passed in "new.txt" it will also match via the self.fixups
+ # field, which will include the "fixup! Commit 1" rename.
+ # But the "fixup ! Commit 1" FixupTargets will only match with
+ # "file.txt" if they occurred before the rename fixup, and will only
+ # match with "new.txt" if they occur after the rename fixup. With the
+ # exception of the rename fixup itself, which will match both.
+ #
+ # In principle, we could identify a file across renames (have a mapping
+ # from each commit to what the file is called at that stage) and match
+ # using this file identifier. Similar to the "--follow" git diff
+ # argument. This would then cover cases where a rename occurs between
+ # the commit and its fixups, and allow fixups before the rename to also
+ # match. However, the former case is unexpected and the latter case
+ # would not be that useful.
+ if self._touches_path_basis(path, filter_status, check_dir):
+ return True
+ # Mark this as a valid target for the path if one of our fixups changes
+ # this path.
+ # NOTE: We use _touch_path_basis to prevent recursion. This means we
+ # will only check one layer up or down, but we only expect fixups of
+ # up to depth 1.
+ for fixup_target in self.fixups:
+ if fixup_target._touches_path_basis(path, filter_status, check_dir):
+ return True
+ # Mark this as a valid target if our target changes this path.
+ if self.target is not None and self.target._touches_path_basis(
+ path, filter_status, check_dir
+ ):
+ return True
+ return False
+
+ def _touches_path_basis(
+ self, path: str, filter_status: None | str, check_dir: bool
+ ) -> bool:
+ """
+ Whether this target touches the given path.
+ :param path: The path to check.
+ :param filter_status: Limit the detected changes to the given status.
+ :param check_dir: Whether we should treat `path` as a directory and check for
+ files within it.
+ :returns: Whether this target matches.
+ """
+ for file_change in self.changes:
+ if filter_status is not None and file_change.status not in filter_status:
+ continue
+ for test_path in (file_change.path, file_change.new_path):
+ if check_dir:
+ if os.path.commonpath((os.path.dirname(test_path), path)) == path:
+ # test_path's directory matches the path or is within it.
+ return True
+ elif test_path == path:
+ return True
+ return False
+
+
+def get_fixup_targets(
+ target_list: list[FixupTarget],
+ from_commit: str,
+ to_commit: str,
+ fixup_depth: int = 0,
+) -> None:
+ """
+ Find all the commits that can be targeted by a fixup between the given
+ commits.
+ :param target_list: The list to fill with targets. Appended in the order of
+ `from_commit` to `to_commit`.
+ :param from_commit: The commit to start from (non-inclusive).
+ :param to_commit: The commit to end on (inclusive).
+ :param fixup_depth: The maximum "depth" of fixups. I.e. how many "fixup!"
+ prefixes to allow.
+ """
+ raw_output = git_get(
+ [
+ "log",
+ "--pretty=format:%H,%h,%s",
+ "--reverse",
+ "--raw",
+ "-z",
+ f"{from_commit}..{to_commit}",
+ ],
+ strip=False,
+ )
+ pretty_regex = re.compile(
+ r"(?P<commit>[0-9a-f]+),(?P<short_ref>[0-9a-f]+),(?P<title>[^\n\0]*)\n"
+ )
+ excluded_regex_list = [
+ re.compile(r"^Bug [0-9]+.*r="), # Backported Mozilla bug.
+ re.compile(r"^dropme! "),
+ ]
+
+ while raw_output:
+ match = pretty_regex.match(raw_output)
+ if not match:
+ raise ValueError(f"Invalid pretty format: {raw_output[:100]}...")
+ fixup_target = FixupTarget(
+ match.group("commit"), match.group("short_ref"), match.group("title")
+ )
+ raw_output = raw_output[match.end() :]
+ while raw_output and raw_output[0] != "\0":
+ file_change, end = parse_raw_diff_line(raw_output)
+ fixup_target.changes.append(file_change)
+ raw_output = raw_output[end:]
+ if raw_output:
+ # Skip over the "\0".
+ raw_output = raw_output[1:]
+
+ for regex in excluded_regex_list:
+ if regex.match(fixup_target.title):
+ # Exclude from the list.
+ continue
+
+ trimmed_title, depth = fixup_target.trim_fixup()
+ if depth:
+ original_target = None
+ for target in target_list:
+ if target.title == trimmed_title:
+ original_target = target
+ break
+
+ if original_target:
+ original_target.fixups.append(fixup_target)
+ fixup_target.target = original_target
+ if depth > fixup_depth:
+ # Exclude from the list.
+ continue
+
+ target_list.append(fixup_target)
+
+
+class NewCommitBasis:
+ def __init__(self) -> None:
+ self.staged_paths: set[str] = set()
+ self.adding_paths: set[str] = set()
+
+ def add(self, paths: Iterable[str], staged: bool) -> None:
+ """
+ Add a path to include in this commit.
+ :param paths: The paths to add.
+ :param staged: Whether we are adding already staged changes.
+ """
+ if staged:
+ self.staged_paths.update(paths)
+ return
+
+ self.adding_paths.update(paths)
+
+
+class NewCommit(NewCommitBasis):
+ """Represents a new commit that we want to create."""
- def parse_log_line(line):
- [commit, short_ref, title] = line.split(",", 2)
- return {"commit": commit, "short-ref": short_ref, "title": title}
+ def __init__(self, alias: str) -> None:
+ """
+ :param alias: The alias name for the commit.
+ """
+ super().__init__()
+ self.alias = alias
- options = [
- parse_log_line(line)
- for line in git_get(
- [
- "log",
- "--pretty=format:%H,%h,%s",
- f"{firefox_commit}..HEAD",
- "--",
- filename,
- ]
+
+class NewFixup(NewCommitBasis):
+ """Represents a new fixup commit that we want to create."""
+
+ def __init__(self, target: FixupTarget) -> None:
+ """
+ :param target: The commit to target with the fixup.
+ """
+ super().__init__()
+ self.target = target
+
+
+def get_suggested_fixup_targets_for_change(
+ file_change: FileChange,
+ fixup_target_list: list[FixupTarget],
+ firefox_directories_lazy: Callable[[], set[str]],
+) -> Iterator[FixupTarget]:
+ """
+ Find the suggested fixup targets for the given file change.
+ :param file_change: The file change to get a suggestion for.
+ :param fixup_target_list: The list to choose from.
+ :param firefox_directories_lazy: Lazy method to return the firefox
+ directories.
+ :yield: The suggested fixup targets.
+ """
+
+ def filter_list(
+ path: str, filter_status: None | str = None, check_dir: bool = False
+ ) -> Iterator[FixupTarget]:
+ return (
+ t
+ for t in fixup_target_list
+ if t.touches_path(path, filter_status=filter_status, check_dir=check_dir)
)
+
+ if file_change.status == "D":
+ # Deleted.
+ # Find the commit that introduced this file or previously deleted it.
+ # I.e. added the file ("A"), renamed it ("R"), or deleted it ("D").
+ yield from filter_list(file_change.path, filter_status="ARD")
+ return
+
+ if file_change.status == "A":
+ # First check to see if this file name was actually touched before.
+ yielded_target = False
+ for target in filter_list(file_change.path):
+ yielded_target = True
+ yield target
+ if yielded_target:
+ return
+ # Else, find commits that introduced files in the same directory, or
+ # deleted in them, if they are not firefox directories.
+ dir_path = file_change.path
+ while True:
+ dir_path = os.path.dirname(dir_path)
+ if not dir_path or dir_path in firefox_directories_lazy():
+ return
+
+ yielded_target = False
+ for target in filter_list(dir_path, filter_status="ARD", check_dir=True):
+ yielded_target = True
+ yield target
+
+ if yielded_target:
+ return
+ # Else, search one directory higher.
+
+ if file_change.status == "R":
+ # Renamed.
+ # Find the commit that introduced the original name for this file.
+ yield from filter_list(file_change.path, filter_status="AR")
+ return
+
+ # Modified.
+ yield from filter_list(file_change.path)
+
+
+def ask_for_target(
+ file_change_list: list[FileChange],
+ new_commits_list: list[NewCommit | NewFixup],
+ suggested_fixup_target_list: list[FixupTarget],
+ full_fixup_target_list: list[FixupTarget],
+ staged: bool = False,
+) -> bool:
+ """
+ Ask the user to choose a target.
+ :param file_change_list: The file changes to ask for.
+ :param new_commits_list: The list of pending new commits, may be added to.
+ :param suggested_fixup_target_list: The list of suggested target fixups
+ to choose from.
+ :param staged: Whether this is for staged changes.
+ :returns: `True` if the operation should be aborted.
+ """
+
+ new_paths = [c.new_path for c in file_change_list]
+ all_paths = set(new_paths).union(c.path for c in file_change_list)
+ non_fixup_commits: list[NewCommit] = [
+ n for n in new_commits_list if isinstance(n, NewCommit)
]
- if not options:
- print(f"No commit found for {filename}")
- return None
- def valid_index(val):
+ shown_list: list[NewCommit | FixupTarget] = (
+ non_fixup_commits + suggested_fixup_target_list
+ )
+
+ can_skip = not staged
+ shown_full = False
+
+ index_offset = 2
+
+ def valid_response(val: str) -> tuple[str, None | NewCommit | FixupTarget]:
+ val = val.strip()
+
+ if val == "h":
+ return "help", None
+
+ if val == "a":
+ return "abort", None
+
if val == "d":
- return val
+ return "diff", None
+
+ if val == "f":
+ if shown_full:
+ # Already done once.
+ raise ValueError()
+ return "full-list", None
+ is_patch_full = val.startswith("P")
is_patch = val.startswith("p")
- if is_patch:
- val = val[1:]
+ if is_patch or is_patch_full:
+ index = int(val[1:], base=10) # Raises ValueError if not integer.
+ else:
+ index = int(val, base=10) # Raises ValueError if not integer.
+ if index == 0:
+ if not can_skip:
+ raise ValueError()
+ return "skip", None
+
+ if index == 1:
+ return "new", None
- # May raise a ValueError.
- as_index = int(val)
- if as_index < 0 or as_index > len(options):
+ index -= index_offset
+
+ if index < 0 or index >= len(shown_list):
raise ValueError()
- if as_index == 0:
- if is_patch:
+ selected = shown_list[index]
+
+ if is_patch_full:
+ return "patch-full", selected
+ if is_patch:
+ return "patch", selected
+ return "target", selected
+
+ def alias_response(val: str) -> str:
+ # Choose a default alias name if none is given.
+ val = val.strip() or f"New commit {len(non_fixup_commits)}"
+ for new_commit in non_fixup_commits:
+ if new_commit.alias == val:
+ # Already in use.
raise ValueError()
- return None
+ return val
+
+ def print_index_option(index: int, description: str) -> None:
+ print(f" \x1b[1m{index}\x1b[0m: {description}")
- return (is_patch, options[as_index - 1]["commit"])
+ def in_pink(text: str) -> str:
+ return f"\x1b[1;38;5;212m{text}\x1b[0m"
+ prefix_str = "For " + (in_pink("staged") if staged else "unstaged") + " changes to"
+ if len(new_paths) == 1:
+ print(f"{prefix_str} {in_pink(new_paths[0])}:")
+ else:
+ print(f"{prefix_str}:")
+ for path in new_paths:
+ print(f" {in_pink(path)}")
+ print("")
+
+ show_help = True
+ reshow_list = True
while True:
- print(f"For {filename}:\n")
- print(" \x1b[1m0\x1b[0m: None")
- for index, opt in enumerate(options):
- print(
- f" \x1b[1m{index + 1}\x1b[0m: "
- + f"\x1b[1;38;5;212m{opt['short-ref']}\x1b[0m "
- + opt["title"]
- )
+ if reshow_list:
+ if can_skip:
+ print_index_option(0, "Skip")
+ print_index_option(1, "New commit")
+ for index, target in enumerate(shown_list, start=index_offset):
+ if isinstance(target, NewCommit):
+ print_index_option(index, f"Add to new commit: {target.alias}")
+ else:
+ print_index_option(
+ index, f"Fixup: {in_pink(target.short_ref)} {target.title}"
+ )
+ reshow_list = False
print("")
- response = prompt_user(
- "Choose an <index> to fixup, or '0' to skip this file, "
- "or 'd' to view the pending diff, "
- "or 'p<index>' to view the patch for the index: ",
- valid_index,
+
+ response, selected = prompt_user(
+ (
+ "Choose an <index> to target. Type 'h' for additional options: "
+ if show_help
+ else "Choose an <index> to target or an option: "
+ ),
+ valid_response,
)
- if response is None:
- # Skip this file.
- return None
- if response == "d":
- git_run(["diff", "--", filename])
+ if response == "help":
+ print("Options:")
+ for option, desc in (
+ ("h", "show the available options."),
+ ("a", "abort this commit operation and all pending commits."),
+ (
+ ("", "")
+ if shown_full
+ else (
+ "f",
+ "show the full list of fixup targets, rather than just the suggested ones.",
+ )
+ ),
+ ("d", "view the diff for the pending file changes."),
+ (
+ "P<index>",
+ "view the patch for the index (including its relevant fixups).",
+ ),
+ (
+ "p<index>",
+ "view the patch for the index (including its relevant fixups), "
+ "limited to the current files.",
+ ),
+ ):
+ if not option:
+ # Skip this option.
+ continue
+ print(f" \x1b[1m{option[0]}\x1b[0m{option[1:].ljust(7)}: {desc}")
+ # Do not show the help option again.
+ show_help = False
+ continue
+
+ if response == "abort":
+ return True
+
+ if response == "skip":
+ return False
+
+ if response == "new":
+ new_alias = prompt_user(
+ "Enter an optional temporary alias for this new commit: ",
+ alias_response,
+ )
+ new_commit = NewCommit(new_alias)
+ new_commit.add(all_paths, staged)
+ new_commits_list.append(new_commit)
+ return False
+
+ if response == "target":
+ assert selected is not None
+
+ if isinstance(selected, NewCommit):
+ # Adding to a new commit.
+ selected.add(all_paths, staged)
+ return False
+
+ for new_fixup in new_commits_list:
+ if not isinstance(new_fixup, NewFixup):
+ continue
+ if new_fixup.target == selected:
+ # We already have a pending fixup commit that targets this
+ # selected target. Add this path to the same commit.
+ new_fixup.add(all_paths, staged)
+ return False
+
+ new_fixup = NewFixup(selected)
+ new_fixup.add(all_paths, staged)
+ new_commits_list.append(new_fixup)
+ return False
+
+ if response == "full-list":
+ shown_list = non_fixup_commits + full_fixup_target_list
+ shown_full = True
+ reshow_list = True
continue
- view_patch, commit = response
- if view_patch:
- git_run(["log", "-p", "-1", commit, "--", filename])
+ if response == "diff":
+ git_args = ["diff", "--color"]
+ if staged:
+ git_args.append("--staged")
+ git_args.extend(git_path_args(all_paths))
+ git_run_pager(git_args)
continue
- return commit
+ if response in ("patch", "patch-full"):
+ assert selected is not None
+
+ filter_paths = response == "patch"
+
+ if isinstance(selected, NewCommit):
+ git_sequence = [
+ ["diff", "--color", "--staged", *git_path_args((path,))]
+ for path in selected.staged_paths
+ if not filter_paths or path in all_paths
+ ]
+ git_sequence.extend(
+ ["diff", "--color", *git_path_args((path,))]
+ for path in selected.adding_paths
+ if not filter_paths or path in all_paths
+ )
+
+ # Show what the expected patch will be for the new commit.
+ git_run_pager(
+ arg_sequence=git_sequence, pager_prefix=f"{selected.alias}\n\n"
+ )
+ else:
+ # Show the log entry for the FixupTarget and each of its fixups.
+ # Order with the commmit closest to HEAD first. We expect
+ # selected.fixups to match this order.
+ git_sequence = []
+ # If `filter_paths` is set, we want to limit the log to the
+ # paths, and try to track any renames in the commit history.
+ prev_log_paths: None | set[str] = None
+ # For the first commit in the sequence, we use the old path
+ # names (rather than `c.new_path`) since we expect the commit
+ # which is closest to us to use the older names.
+ log_paths: None | set[str] = (
+ {c.path for c in file_change_list} if filter_paths else None
+ )
+ for target in (*selected.fixups, selected):
+ git_args = [
+ "log",
+ "--color",
+ "-p",
+ f"{target.commit}~1..{target.commit}",
+ ]
+ if filter_paths:
+ assert log_paths is not None
+ # Track the renamed paths.
+ prev_log_paths = log_paths.copy()
+ for file_change in target.changes:
+ if (
+ file_change.status == "R"
+ and file_change.new_path in log_paths
+ ):
+ # file was renamed in this change.
+ # Update log_paths to the new name.
+ # NOTE: This should have a similar effect to the
+ # --follow option for git log for a single file
+ # NOTE: File renames will not be properly
+ # tracked if a rename occurs outside of
+ # `selected.changes` or
+ # `selected.fixups[].changes`, but this is
+ # unexpected.
+ log_paths.remove(file_change.new_path)
+ log_paths.add(file_change.path)
+
+ # NOTE: This log entry may be empty if none of the paths
+ # match.
+ # NOTE: We include both log_paths and prev_log_paths to
+ # show renames in the diff output.
+ git_args.extend(git_path_args(log_paths | prev_log_paths))
+ git_sequence.append(git_args)
+ # Combine all the logs into one.
+ git_run_pager(arg_sequence=git_sequence)
+ continue
+
+ raise ValueError(f"Unexpected response: {response}")
-def auto_fixup(_args):
+def auto_commit(_args: argparse.Namespace) -> None:
"""
- Automatically find and fix up commits using the current unstaged changes.
+ Automatically find and fix up commits for any pending changes.
"""
+ # Want git log and add to be run from the root.
+ os.chdir(get_local_root())
# Only want to search as far back as the firefox commit.
firefox_commit = get_firefox_ref("HEAD").commit
- staged_files = get_changed_files("HEAD", staged=True)
- if staged_files:
- raise TbDevException(f"Have already staged files: {staged_files}")
+ staged_changes = [f for f in get_changed_files(staged=True)]
+ if staged_changes:
+ print("Existing staged changes for:")
+ for file_change in staged_changes:
+ print(f" {file_change.new_path}")
+ if not prompt_user(
+ "Include staged changes? (y/\x1b[4mn\x1b[0m)", binary_reply_default_no
+ ):
+ raise TbDevException("Cannot continue with pending staged changes")
+ print("")
- fixups = {}
- for filename in get_changed_files("HEAD"):
- commit = get_fixup_for_file(filename, firefox_commit)
- if commit is None:
+ full_target_list: list[FixupTarget] = []
+ # Determine if HEAD points to a branch or not and has an upstream commit.
+ # We choose check=False since the exit status is non-zero when we are in a
+ # detached state.
+ head_symbolic_ref = git_get(["symbolic-ref", "-q", "HEAD"], check=False)
+ if not head_symbolic_ref or not bool(
+ git_get(["for-each-ref", "--format=%(upstream)", head_symbolic_ref])
+ ):
+ # Unexpected, but not fatal.
+ print("HEAD has no upstream tracking!")
+ # Just include all commits since firefox_commit with no fixup depth
+ get_fixup_targets(full_target_list, firefox_commit, "HEAD", fixup_depth=0)
+ else:
+ upstream_commit = get_upstream_basis_commit("HEAD")
+ # Only include "fixup!" commits that are between here and the upstream
+ # tracking commit.
+ get_fixup_targets(
+ full_target_list, firefox_commit, upstream_commit, fixup_depth=0
+ )
+ get_fixup_targets(full_target_list, upstream_commit, "HEAD", fixup_depth=1)
+
+ # full_target_list is ordered with the earlier commits first. Reverse this.
+ full_target_list.reverse()
+ # Also reverse the fixups order to follow the same order.
+ for target in full_target_list:
+ target.fixups.reverse()
+
+ # Lazy load the list of firefox directories since they are unlikely to be
+ # needed.
+ @functools.cache
+ def firefox_directories_lazy() -> set[str]:
+ return {
+ dir_name
+ for dir_name in git_get(
+ [
+ "ls-tree",
+ "-r",
+ "-d",
+ "--name-only",
+ "--full-tree",
+ "-z",
+ firefox_commit,
+ ],
+ strip=False,
+ ).split("\0")
+ if dir_name
+ }
+
+ # Check untracked files to be added.
+ for path in git_get(
+ ["ls-files", "--other", "--exclude-standard", "-z"], strip=False
+ ).split("\0"):
+ if not path:
continue
- if commit not in fixups:
- fixups[commit] = [filename]
- else:
- fixups[commit].append(filename)
+ if prompt_user(
+ f"Start tracking file `{path}`? (y/\x1b[4mn\x1b[0m)",
+ binary_reply_default_no,
+ ):
+ # Include in the git diff output, but do not stage.
+ git_run(["add", "--intent-to-add", path])
print("")
- for commit, files in fixups.items():
- print("")
- git_run(["add", *files])
- git_run(["commit", f"--fixup={commit}"])
+ aborted = False
+ new_commits_list: list[NewCommit | NewFixup] = []
+ # First go through staged changes.
+ if staged_changes:
+ common_fixup_targets = None
+ for change in staged_changes:
+ target_iter = get_suggested_fixup_targets_for_change(
+ change, full_target_list, firefox_directories_lazy
+ )
+ if common_fixup_targets is None:
+ common_fixup_targets = set(target_iter)
+ else:
+ common_fixup_targets.intersection_update(target_iter)
+
+ assert common_fixup_targets is not None
+
+ aborted = ask_for_target(
+ staged_changes,
+ new_commits_list,
+ # Sort in the same order as full_target_list.
+ [target for target in full_target_list if target in common_fixup_targets],
+ full_target_list,
+ staged=True,
+ )
print("")
- if prompt_user(
- "Edit fixup commit message? (y/\x1b[4mn\x1b[0m)", binary_reply_default_no
- ):
+ if not aborted:
+ for file_change in get_changed_files():
+ target_list = list(
+ get_suggested_fixup_targets_for_change(
+ file_change, full_target_list, firefox_directories_lazy
+ )
+ )
+ aborted = ask_for_target(
+ [file_change],
+ new_commits_list,
+ target_list,
+ full_target_list,
+ staged=False,
+ )
+ print("")
+ if aborted:
+ break
+
+ if aborted:
+ return
+
+ # NOTE: Only the first commit can include staged changes.
+ # This should already be the case, but we want to double check.
+ for commit_index in range(1, len(new_commits_list)):
+ if new_commits_list[commit_index].staged_paths:
+ raise ValueError(f"Staged changes for commit {commit_index}")
+
+ for new_commit in new_commits_list:
+ print("")
+ if new_commit.adding_paths:
+ git_run(["add", *git_path_args(new_commit.adding_paths)])
+ if isinstance(new_commit, NewFixup):
+ git_run(["commit", f"--fixup={new_commit.target.commit}"])
+ print("")
+ is_double_fixup = bool(new_commit.target.target)
+ if not is_double_fixup and prompt_user(
+ "Edit fixup commit message? (y/\x1b[4mn\x1b[0m)",
+ binary_reply_default_no,
+ ):
+ git_run(["commit", "--amend"])
+ print("")
+ else:
+ git_run(["commit", "-m", new_commit.alias])
git_run(["commit", "--amend"])
+ print("")
-def clean_fixups(_args):
+def clean_fixups(_args: argparse.Namespace) -> None:
"""
Perform an interactive rebase that automatically applies fixups, similar to
--autosquash but also works on fixups of fixups.
"""
- user_editor = git_get(["var", "GIT_SEQUENCE_EDITOR"])[0]
+ user_editor = git_get(["var", "GIT_SEQUENCE_EDITOR"])
sub_editor = os.path.join(
os.path.dirname(os.path.realpath(__file__)), FIXUP_PREPROCESSOR_EDITOR
)
@@ -525,7 +1326,7 @@ def clean_fixups(_args):
)
-def show_default(_args):
+def show_default(_args: argparse.Namespace) -> None:
"""
Print the default branch name from gitlab.
"""
@@ -536,7 +1337,7 @@ def show_default(_args):
print(f"{upstream}/{default_branch}")
-def branch_from_default(args):
+def branch_from_default(args: argparse.Namespace) -> None:
"""
Fetch the default gitlab branch from upstream and create a new local branch.
"""
@@ -557,7 +1358,7 @@ def branch_from_default(args):
)
-def move_to_default(args):
+def move_to_default(args: argparse.Namespace) -> None:
"""
Fetch the default gitlab branch from upstream and move the specified
branch's commits on top. A new branch will be created tracking the default
@@ -569,7 +1370,7 @@ def move_to_default(args):
if branch_name is None:
# Use current branch as default.
try:
- branch_name = git_get(["branch", "--show-current"])[0]
+ branch_name = git_get(["branch", "--show-current"])
except IndexError:
raise TbDevException("No current branch")
@@ -608,7 +1409,7 @@ def move_to_default(args):
git_run(["cherry-pick", f"{current_basis}..{old_branch_name}"], check=False)
-def show_range_diff(args):
+def show_range_diff(args: argparse.Namespace) -> None:
"""
Show the range diff between two branches, from their firefox bases.
"""
@@ -624,21 +1425,21 @@ def show_range_diff(args):
)
-def show_diff_diff(args):
+def show_diff_diff(args: argparse.Namespace) -> None:
"""
Show the diff between the diffs of two branches, relative to their firefox
bases.
"""
- config_res = git_get(["config", "--get", "diff.tool"])
- if not config_res:
+ try:
+ diff_tool = next(git_lines(["config", "--get", "diff.tool"]))
+ except StopIteration:
raise TbDevException("No diff.tool configured for git")
- diff_tool = config_res[0]
# Filter out parts of the diff we expect to be different.
index_regex = re.compile(r"index [0-9a-f]{12}\.\.[0-9a-f]{12}")
lines_regex = re.compile(r"@@ -[0-9]+,[0-9]+ \+[0-9]+,[0-9]+ @@(?P<rest>.*)")
- def save_diff(branch):
+ def save_diff(branch: str) -> str:
firefox_commit = get_firefox_ref(branch).commit
file_desc, file_name = tempfile.mkstemp(
text=True, prefix=f'{branch.split("/")[-1]}-'
@@ -653,6 +1454,7 @@ def show_diff_diff(args):
)
with os.fdopen(file_desc, "w") as file:
+ assert diff_process.stdout is not None
for line in diff_process.stdout:
if index_regex.match(line):
# Fake data that will match.
@@ -665,7 +1467,7 @@ def show_diff_diff(args):
continue
file.write(line)
- status = diff_process.poll()
+ status = diff_process.wait()
if status != 0:
raise TbDevException(f"git diff exited with status {status}")
@@ -681,7 +1483,7 @@ def show_diff_diff(args):
# * -------------------- *
-def branch_complete(prefix, parsed_args, **kwargs):
+def branch_complete(prefix: str, **_kwargs: Any) -> list[str]:
"""
Complete the argument with a branch name.
"""
@@ -689,7 +1491,7 @@ def branch_complete(prefix, parsed_args, **kwargs):
return []
try:
branches = [ref.name for ref in get_refs("head", "")]
- branches.extend([ref.name for ref in get_refs("remote", "")])
+ branches.extend(ref.name for ref in get_refs("remote", ""))
branches.append("HEAD")
except Exception:
return []
@@ -699,7 +1501,20 @@ def branch_complete(prefix, parsed_args, **kwargs):
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(required=True)
-for name, details in {
+
+class ArgConfig(TypedDict):
+ help: str
+ metavar: NotRequired[str]
+ nargs: NotRequired[str]
+ completer: NotRequired[Callable[[str], list[str]]]
+
+
+class CommandConfig(TypedDict):
+ func: Callable[[argparse.Namespace], None]
+ args: NotRequired[dict[str, ArgConfig]]
+
+
+all_commands: dict[str, CommandConfig] = {
"show-upstream-basis-commit": {
"func": show_upstream_basis_commit,
},
@@ -716,8 +1531,8 @@ for name, details in {
},
},
},
- "auto-fixup": {
- "func": auto_fixup,
+ "auto-commit": {
+ "func": auto_commit,
},
"clean-fixups": {
"func": clean_fixups,
@@ -794,20 +1609,25 @@ for name, details in {
"regex": {"help": "the regex that the files must contain"},
},
},
-}.items():
- help_message = re.sub(r"\s+", " ", details["func"].__doc__).strip()
+}
+
+for name, command_config in all_commands.items():
+ help_message = command_config["func"].__doc__
+ assert isinstance(help_message, str)
+ help_message = re.sub(r"\s+", " ", help_message).strip()
sub = subparsers.add_parser(name, help=help_message)
- sub.set_defaults(func=details["func"])
- for arg, keywords in details.get("args", {}).items():
+ sub.set_defaults(func=command_config["func"])
+ for arg, keywords in command_config.get("args", {}).items():
completer = None
if "completer" in keywords:
completer = keywords["completer"]
del keywords["completer"]
sub_arg = sub.add_argument(arg, **keywords)
- if completer is not None:
- sub_arg.completer = completer
+ if completer is not None and argcomplete is not None:
+ sub_arg.completer = completer # type: ignore
-argcomplete.autocomplete(parser)
+if argcomplete is not None:
+ argcomplete.autocomplete(parser)
try:
if not within_browser_root():
View it on GitLab: https://gitlab.torproject.org/tpo/applications/mullvad-browser/-/compare/08…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/mullvad-browser/-/compare/08…
You're receiving this email because of your account on gitlab.torproject.org.
1
0
[Git][tpo/applications/tor-browser][base-browser-146.0a1-16.0-2] 7 commits: fixup! BB 41803: Add some developer tools for working on tor-browser.
by henry (@henry) 11 Dec '25
by henry (@henry) 11 Dec '25
11 Dec '25
henry pushed to branch base-browser-146.0a1-16.0-2 at The Tor Project / Applications / Tor Browser
Commits:
5138b860 by Henry Wilkes at 2025-12-11T14:43:50+00:00
fixup! BB 41803: Add some developer tools for working on tor-browser.
TB 44367: Make git_get return the stdout string, rather than a list.
Add git_lines to generate lines.
- - - - -
3aff7d98 by Henry Wilkes at 2025-12-11T14:43:51+00:00
fixup! BB 41803: Add some developer tools for working on tor-browser.
TB 44367: Use raw diff to get list of file changes.
- - - - -
5ba0688d by Henry Wilkes at 2025-12-11T14:43:52+00:00
fixup! BB 41803: Add some developer tools for working on tor-browser.
TB 44367: Add type annotations and parameter documentation.
- - - - -
08c1d176 by Henry Wilkes at 2025-12-11T14:43:53+00:00
fixup! BB 41803: Add some developer tools for working on tor-browser.
TB 44367: Make the argcomplete module optional.
- - - - -
ebac78a8 by Henry Wilkes at 2025-12-11T14:43:54+00:00
fixup! BB 41803: Add some developer tools for working on tor-browser.
TB 44367: Use function caching instead of global variables.
- - - - -
12776f40 by Henry Wilkes at 2025-12-11T14:43:55+00:00
fixup! BB 41803: Add some developer tools for working on tor-browser.
TB 44367: Fetch FIREFOX_ tags from the remote if they are missing.
- - - - -
18b3272d by Henry Wilkes at 2025-12-11T14:43:56+00:00
fixup! BB 41803: Add some developer tools for working on tor-browser.
TB 44367: Improve the auto-fixup/auto-commit command.
The auto-fixup command was renamed to auto-commit. It now also handles:
1. Already staged changes.
2. Untracked/added files.
3. Removed files.
4. Renamed files.
5. Allowing the user to create a new commit.
- - - - -
1 changed file:
- tools/base_browser/tb-dev
Changes:
=====================================
tools/base_browser/tb-dev
=====================================
@@ -6,6 +6,7 @@ Useful tools for working on tor-browser repository.
import argparse
import atexit
+import functools
import json
import os
import re
@@ -14,8 +15,15 @@ import sys
import tempfile
import termios
import urllib.request
+from collections.abc import Callable, Iterable, Iterator
+from types import ModuleType
+from typing import Any, NotRequired, TypedDict, TypeVar
-import argcomplete
+argcomplete: None | ModuleType = None
+try:
+ import argcomplete
+except ImportError:
+ pass
GIT_PATH = "/usr/bin/git"
UPSTREAM_URLS = {
@@ -36,9 +44,14 @@ class TbDevException(Exception):
pass
-def git_run(args, check=True, env=None):
+def git_run(
+ args: list[str], check: bool = True, env: None | dict[str, str] = None
+) -> None:
"""
Run a git command with output sent to stdout.
+ :param args: The arguments to pass to git.
+ :param check: Whether to check for success.
+ :param env: Optional environment to set.
"""
if env is not None:
tmp_env = dict(os.environ)
@@ -51,46 +64,122 @@ def git_run(args, check=True, env=None):
raise TbDevException(str(err)) from err
-def git_get(args):
+def git_run_pager(
+ args: list[str] | None = None,
+ arg_sequence: Iterable[list[str]] | None = None,
+ pager_prefix: None | str = None,
+) -> None:
"""
- Run a git command with each non-empty line returned in a list.
+ Run a sequence of git commands with the output concatenated and sent to the
+ git pager.
+ :param args: The arguments to pass to git, or `None` if a sequence is desired.
+ :param arg_sequence: A sequence representing several git commands.
+ :param pager_prefix: An optional text to send to the pager first.
+ """
+ if arg_sequence is None:
+ if args is not None:
+ arg_sequence = (args,)
+ else:
+ raise ValueError("Missing `arg_sequence` or `args`")
+ elif args is not None:
+ raise ValueError("Unexpected both args and arg_sequence")
+
+ pager = git_get(["var", "GIT_PAGER"])
+ if not pager:
+ raise TbDevException("Missing a GIT_PAGER")
+ command = [pager]
+ if os.path.basename(pager) == "less":
+ # Show colours.
+ command.append("-R")
+
+ pager_process = subprocess.Popen(command, stdin=subprocess.PIPE, text=True)
+ assert pager_process.stdin is not None
+
+ if pager_prefix is not None:
+ pager_process.stdin.write(pager_prefix)
+ pager_process.stdin.flush()
+
+ for git_args in arg_sequence:
+ subprocess.run(
+ [GIT_PATH, "--no-pager", *git_args], check=False, stdout=pager_process.stdin
+ )
+
+ pager_process.stdin.close()
+
+ status = pager_process.wait()
+ if status != 0:
+ raise TbDevException(f"git pager {pager} exited with status {status}")
+
+
+def git_get(args: list[str], strip: bool = True, check: bool = True) -> str:
+ """
+ Return the output from a git command.
+ :param args: The arguments to send to git.
+ :param strip: Whether to strip the whitespace from the output.
+ :param check: Whether to check for success.
+ :returns: The stdout.
"""
try:
git_process = subprocess.run(
- [GIT_PATH, *args], text=True, stdout=subprocess.PIPE, check=True
+ [GIT_PATH, *args], text=True, stdout=subprocess.PIPE, check=check
)
except subprocess.CalledProcessError as err:
raise TbDevException(str(err)) from err
- return [line for line in git_process.stdout.split("\n") if line]
+ ret = git_process.stdout
+ if strip:
+ ret = ret.strip()
+ return ret
-local_root = None
+def git_lines(args: list[str]) -> Iterator[str]:
+ """
+ Yields the non-empty lines returned by the git command.
+ :param args: The arguments to send to git.
+ :yield: The lines.
+ """
+ for line in git_get(args, strip=False).split("\n"):
+ if not line:
+ continue
+ yield line
+
+
+def git_path_args(path_iter: Iterable[str]) -> Iterator[str]:
+ """
+ Generate the trailing arguments to specify paths in git commands, includes
+ the "--" separator just before the paths.
+ :param path_iter: The paths that should be passed in.
+ :yields: The git arguments.
+ """
+ yield "--"
+ for path in path_iter:
+ yield f":(literal){path}"
-def get_local_root():
+(a)functools.cache
+def get_local_root() -> str:
"""
Get the path for the tor-browser root directory.
+ :returns: The local root.
"""
- global local_root
- if local_root is None:
- try:
- # Make sure we have a matching remote in this git repository.
- if get_upstream_details()["is-browser-repo"]:
- local_root = git_get(["rev-parse", "--show-toplevel"])[0]
- else:
- local_root = ""
- except TbDevException:
- local_root = ""
- return local_root
+ try:
+ # Make sure we have a matching remote in this git repository.
+ if get_upstream_details()["is-browser-repo"] == "True":
+ return git_get(["rev-parse", "--show-toplevel"])
+ else:
+ return ""
+ except TbDevException:
+ return ""
-def determine_upstream_details():
+(a)functools.cache
+def get_upstream_details() -> dict[str, str]:
"""
- Determine details about the upstream.
+ Get details about the upstream repository.
+ :returns: The details.
"""
remote_urls = {
- remote: git_get(["remote", "get-url", remote])[0]
- for remote in git_get(["remote"])
+ remote: git_get(["remote", "get-url", remote])
+ for remote in git_lines(["remote"])
}
matches = {
@@ -102,7 +191,7 @@ def determine_upstream_details():
}
is_browser_repo = len(matches) > 0
- details = {"is-browser-repo": is_browser_repo}
+ details = {"is-browser-repo": str(is_browser_repo)}
origin_remote_repo = matches.get("origin", None)
upstream_remote_repo = matches.get("upstream", None)
@@ -125,31 +214,30 @@ def determine_upstream_details():
return details
-cached_upstream_details = None
-
-
-def get_upstream_details():
- """
- Get details about the upstream repository.
- """
- global cached_upstream_details
- if cached_upstream_details is None:
- cached_upstream_details = determine_upstream_details()
- return cached_upstream_details
-
-
class Reference:
"""Represents a git reference to a commit."""
- def __init__(self, name, commit):
- self.name = name
+ _REFS_REGEX = re.compile(r"refs/[a-z]+/")
+
+ def __init__(self, full_name: str, commit: str) -> None:
+ """
+ :param full_name: The full reference name. E.g. "refs/tags/MyTag".
+ :param commit: The commit hash for the commit this reference points to.
+ """
+ match = self.__class__._REFS_REGEX.match(full_name)
+ if not match:
+ raise ValueError(f"Invalid reference name {full_name}")
+ self.full_name = full_name
+ self.name = full_name[match.end() :]
self.commit = commit
-def get_refs(ref_type, name_start):
+def get_refs(ref_type: str, name_start: str) -> Iterator[Reference]:
"""
- Get a list of references that match the given 'ref_type' ("tag" or "remote"
- or "head") that starts with the given 'name_start'.
+ Get a list of references that match the given conditions.
+ :param ref_type: The ref type to search for ("tag" or "remote" or "head").
+ :param name_start: The ref name start to match against.
+ :yield: The matching references.
"""
if ref_type == "tag":
ref_start = "refs/tags/"
@@ -163,56 +251,83 @@ def get_refs(ref_type, name_start):
fstring = "%(*objectname),%(objectname),%(refname)"
pattern = f"{ref_start}{name_start}**"
- def line_to_ref(line):
+ def line_to_ref(line: str) -> Reference:
[objectname_reference, objectname, ref_name] = line.split(",", 2)
# For annotated tags, the objectname_reference is non-empty and points
# to an actual commit.
# For remotes, heads and lightweight tags, the objectname_reference will
# be empty and objectname will point directly to the commit.
- return Reference(
- ref_name.replace(ref_start, "", 1), objectname_reference or objectname
- )
+ return Reference(ref_name, objectname_reference or objectname)
- return [
+ return (
line_to_ref(line)
- for line in git_get(["for-each-ref", f"--format={fstring}", pattern])
- ]
+ for line in git_lines(["for-each-ref", f"--format={fstring}", pattern])
+ )
-def get_nearest_ref(ref_type, name_start, search_from):
+def get_firefox_ref(search_from: str) -> Reference:
"""
- Search backwards from the 'search_from' commit to find the first commit
- that matches the given 'ref_type' that starts with the given 'name_start'.
+ Search for the commit that comes from firefox.
+ :param search_from: The commit to search backwards from.
+ :returns: The firefox reference.
"""
- ref_list = get_refs(ref_type, name_start)
+ # Only search a limited history that should include the FIREFOX_ tag.
+ search_commits = [c for c in git_lines(["rev-list", "-1000", search_from])]
+
+ firefox_tag_prefix = "FIREFOX_"
- for commit in git_get(["rev-list", "-1000", search_from]):
- for ref in ref_list:
+ existing_tags = list(get_refs("tag", firefox_tag_prefix))
+ for commit in search_commits:
+ for ref in existing_tags:
if commit == ref.commit:
return ref
- raise TbDevException(f"No {name_start} commit found in the last 1000 commits")
-
-
-def get_firefox_ref(search_from):
+ # Might just need to fetch tags from the remote.
+ upstream = get_upstream_details().get("remote", None)
+ if upstream:
+ remote_ref: None | Reference = None
+ search_index = len(search_commits)
+ # Search the remote for a tag that is in our history.
+ # We want to avoid triggering a long fetch, so we just want to grab the
+ # tag that already points to a commit in our history.
+ for line in git_lines(
+ ["ls-remote", upstream, f"refs/tags/{firefox_tag_prefix}*"]
+ ):
+ objectname, name = line.split("\t", 1)
+ for index in range(search_index):
+ if search_commits[index] == objectname:
+ # Remove trailing "^{}" for commits pointed to by
+ # annotated tags.
+ remote_ref = Reference(re.sub(r"\^\{\}$", "", name), objectname)
+ # Only continue to search for references that are even
+ # closer to `search_from`.
+ search_index = index
+ break
+ if remote_ref is not None:
+ # Get a local copy of just this tag.
+ git_run(["fetch", "--no-tags", upstream, "tag", remote_ref.name])
+ return ref
+
+ raise TbDevException("Unable to find FIREFOX_ tag")
+
+
+def get_upstream_tracking_branch(search_from: str) -> str:
"""
- Search backwards from the 'search_from' commit to find the commit that comes
- from firefox.
+ :param search_from: The commit reference.
+ :returns: The upstream branch reference name.
"""
- return get_nearest_ref("tag", "FIREFOX_", search_from)
-
-
-def get_upstream_tracking_branch(search_from):
- return git_get(["rev-parse", "--abbrev-ref", f"{search_from}@{{upstream}}"])[0]
+ return git_get(["rev-parse", "--abbrev-ref", f"{search_from}@{{upstream}}"])
-def get_upstream_basis_commit(search_from):
+def get_upstream_basis_commit(search_from: str) -> str:
"""
Get the first common ancestor of search_from that is also in its upstream
branch.
+ :param search_from: The commit reference.
+ :returns: The upstream commit hash.
"""
upstream_branch = get_upstream_tracking_branch(search_from)
- commit = git_get(["merge-base", search_from, upstream_branch])[0]
+ commit = git_get(["merge-base", search_from, upstream_branch])
# Verify that the upstream commit shares the same firefox basis. Otherwise,
# this would indicate that the upstream is on an early or later FIREFOX
# base.
@@ -226,26 +341,82 @@ def get_upstream_basis_commit(search_from):
return commit
-def get_changed_files(from_commit, staged=False):
+class FileChange:
+ """Represents a git change to a commit."""
+
+ def __init__(self, status: str, path: str, new_path: str) -> None:
+ """
+ :param status: The file change status used within git diff. E.g. "M" for
+ modified, or "D" for deleted.
+ :param path: The source file path.
+ :param new_path: The file path after the change.
+ """
+ self.status = status
+ self.path = path
+ self.new_path = new_path
+
+
+RAW_DIFF_PATH_PATTERN = r"(?P<path>[^\0]*)\0"
+RAW_DIFF_LINE_REGEX = re.compile(
+ r":[0-7]+ [0-7]+ [0-9a-f]+ [0-9a-f]+ (?P<status>[ADMTUXRC])[0-9]*\0"
+ + RAW_DIFF_PATH_PATTERN
+)
+RAW_DIFF_PATH_REGEX = re.compile(RAW_DIFF_PATH_PATTERN)
+
+
+def parse_raw_diff_line(raw_output: str) -> tuple[FileChange, int]:
"""
- Get a list of filenames relative to the current working directory that have
+ Parse the --raw diff output from git.
+ :param raw_output: The raw output.
+ :returns: The change for this line, and the offset for the end of the raw
+ diff line.
+ """
+ match = RAW_DIFF_LINE_REGEX.match(raw_output)
+ if not match:
+ raise ValueError(f"Invalid raw output: {raw_output[:50]}...")
+ path = os.path.relpath(os.path.join(get_local_root(), match.group("path")))
+ status = match.group("status")
+ if status in ("R", "C"):
+ match = RAW_DIFF_PATH_REGEX.match(raw_output, pos=match.end())
+ if not match:
+ raise ValueError(f"Invalid raw output for rename: {raw_output[:50]}...")
+ new_path = os.path.relpath(os.path.join(get_local_root(), match.group("path")))
+ else:
+ new_path = path
+
+ return FileChange(status, path, new_path), match.end()
+
+
+def get_changed_files(
+ from_commit: None | str = None, staged: bool = False
+) -> Iterator[FileChange]:
+ """
+ Get a list of file changes relative to the current working directory that have
been changed since 'from_commit' (non-inclusive).
+ :param from_commit: The commit to compare against, otherwise use the git
+ diff default.
+ :param staged: Whether to limit the diff to staged changes.
+ :yield: The file changes.
"""
- args = ["diff"]
+ args = ["diff", "-z", "--raw"]
if staged:
args.append("--staged")
- args.append("--name-only")
- args.append(from_commit)
- return [
- os.path.relpath(os.path.join(get_local_root(), filename))
- for filename in git_get(args)
- ]
+ if from_commit:
+ args.append(from_commit)
+ raw_output = git_get(args, strip=False)
+ while raw_output:
+ file_change, end = parse_raw_diff_line(raw_output)
+ yield file_change
+ raw_output = raw_output[end:]
-def file_contains(filename, regex):
+def file_contains(filename: str, regex: re.Pattern[str]) -> bool:
"""
Return whether the file is a utf-8 text file containing the regular
expression given by 'regex'.
+ :param filename: The file path.
+ :param regex: The pattern to search for.
+ :returns: Whether the pattern was matched.
"""
with open(filename, encoding="utf-8") as file:
try:
@@ -258,9 +429,10 @@ def file_contains(filename, regex):
return False
-def get_gitlab_default():
+def get_gitlab_default() -> str:
"""
Get the name of the default branch on gitlab.
+ :returns: The branch name.
"""
repo_name = get_upstream_details().get("repo-name", None)
if repo_name is None:
@@ -283,12 +455,14 @@ def get_gitlab_default():
)
with urllib.request.urlopen(gitlab_request, timeout=20) as response:
- return json.load(response)["data"]["project"]["repository"]["rootRef"]
+ default = json.load(response)["data"]["project"]["repository"]["rootRef"]
+ assert isinstance(default, str)
+ return default
-def within_browser_root():
+def within_browser_root() -> bool:
"""
- Whether we are with the tor browser root.
+ :returns: Whether we are with the tor browser root.
"""
root = get_local_root()
if not root:
@@ -301,24 +475,24 @@ def within_browser_root():
# * -------------------- *
-def show_firefox_commit(_args):
+def show_firefox_commit(_args: argparse.Namespace) -> None:
"""
Print the tag name and commit for the last firefox commit below the current
HEAD.
"""
ref = get_firefox_ref("HEAD")
- print(ref.name)
+ print(ref.full_name)
print(ref.commit)
-def show_upstream_basis_commit(_args):
+def show_upstream_basis_commit(_args: argparse.Namespace) -> None:
"""
Print the last upstream commit for the current HEAD.
"""
print(get_upstream_basis_commit("HEAD"))
-def show_log(args):
+def show_log(args: argparse.Namespace) -> None:
"""
Show the git log between the current HEAD and the last firefox commit.
"""
@@ -326,7 +500,7 @@ def show_log(args):
git_run(["log", f"{commit}..HEAD", *args.gitargs], check=False)
-def show_files_containing(args):
+def show_files_containing(args: argparse.Namespace) -> None:
"""
List all the files that that have been modified for tor browser, that also
contain a regular expression.
@@ -336,33 +510,32 @@ def show_files_containing(args):
except re.error as err:
raise TbDevException(f"{args.regex} is not a valid python regex") from err
- file_list = get_changed_files(get_firefox_ref("HEAD").commit)
-
- for filename in file_list:
- if not os.path.isfile(filename):
+ for file_change in get_changed_files(get_firefox_ref("HEAD").commit):
+ path = file_change.new_path
+ if not os.path.isfile(path):
# deleted ofile
continue
- if file_contains(filename, regex):
- print(filename)
+ if file_contains(path, regex):
+ print(path)
-def show_changed_files(_args):
+def show_changed_files(_args: argparse.Namespace) -> None:
"""
List all the files that have been modified relative to upstream.
"""
- for filename in get_changed_files(get_upstream_basis_commit("HEAD")):
- print(filename)
+ for file_change in get_changed_files(get_upstream_basis_commit("HEAD")):
+ print(file_change.new_path)
-def lint_changed_files(args):
+def lint_changed_files(args: argparse.Namespace) -> None:
"""
Lint all the files that have been modified relative to upstream.
"""
os.chdir(get_local_root())
file_list = [
- f
+ f.new_path
for f in get_changed_files(get_upstream_basis_commit("HEAD"))
- if os.path.isfile(f) # Not deleted
+ if os.path.isfile(f.new_path) # Not deleted
]
# We add --warnings since clang only reports whitespace issues as warnings.
subprocess.run(
@@ -371,10 +544,18 @@ def lint_changed_files(args):
)
-def prompt_user(prompt, convert):
+# TODO: replace with "prompt_user[T](..., T]) -> T" after python 3.12 is the
+# minimum mach version.
+T = TypeVar("T")
+
+
+def prompt_user(prompt: str, convert: Callable[[str], T]) -> T:
"""
- Ask the user for some input until the given converter returns without
- throwing a ValueError.
+ Ask the user for some input.
+ :param prompt: The prompt to show the user.
+ :param convert: A method to convert the response into a type. Should
+ throw `ValueError` if the user should be re-prompted for a valid input.
+ :returns: The first valid user response.
"""
while True:
# Flush out stdin.
@@ -388,8 +569,12 @@ def prompt_user(prompt, convert):
pass
-def binary_reply_default_no(value):
- """Process a 'y' or 'n' reply, defaulting to 'n' if empty."""
+def binary_reply_default_no(value: str) -> bool:
+ """
+ Process a 'y' or 'n' reply, defaulting to 'n' if empty.
+ :param value: The user input.
+ :returns: Whether the answer is yes.
+ """
if value == "":
return False
if value.lower() == "y":
@@ -399,121 +584,737 @@ def binary_reply_default_no(value):
raise ValueError()
-def get_fixup_for_file(filename, firefox_commit):
- """Find the commit the given file should fix up."""
+class FixupTarget:
+ """Represents a commit that can be targeted by a fixup."""
+
+ def __init__(self, commit: str, short_ref: str, title: str) -> None:
+ """
+ :param commit: The commit hash for the commit.
+ :param short_ref: The shortened commit hash for display.
+ :param title: The first line of the commit message.
+ """
+ self.commit = commit
+ self.short_ref = short_ref
+ self.title = title
+ self.changes: list[FileChange] = []
+ self.fixups: list[FixupTarget] = []
+ self.target: None | FixupTarget = None
+
+ _FIXUP_REGEX = re.compile(r"^fixup! +")
+
+ def trim_fixup(self) -> tuple[str, int]:
+ """
+ Trim the "fixup!" prefixes.
+ :returns: The stripped commit title and the fixup depth (how many fixups
+ prefixes there were).
+ """
+ title = self.title
+ depth = 0
+ while True:
+ match = self.__class__._FIXUP_REGEX.match(title)
+ if not match:
+ return title, depth
+ title = title[match.end() :]
+ depth += 1
+
+ def touches_path(
+ self, path: str, filter_status: None | str = None, check_dir: bool = False
+ ) -> bool:
+ """
+ Whether this target, or one of its fixups or target, touches the given
+ path.
+ :param path: The path to check.
+ :param filter_status: Limit the detected changes to the given status(es).
+ :param check_dir: Whether we should treat `path` as a directory and check for
+ files within it.
+ :returns: Whether this target matches.
+ """
+ # NOTE: In the case of renames, we generally assume that renames occur
+ # in the fixup targets. E.g. "Commit 1" creates the file "file.txt", and
+ # "fixup! Commit 1" renames it to "new.txt". In this case, if the
+ # FixupTarget for "Commit 1" is passed in "file.txt" it will match. And
+ # if it is passed in "new.txt" it will also match via the self.fixups
+ # field, which will include the "fixup! Commit 1" rename.
+ # But the "fixup ! Commit 1" FixupTargets will only match with
+ # "file.txt" if they occurred before the rename fixup, and will only
+ # match with "new.txt" if they occur after the rename fixup. With the
+ # exception of the rename fixup itself, which will match both.
+ #
+ # In principle, we could identify a file across renames (have a mapping
+ # from each commit to what the file is called at that stage) and match
+ # using this file identifier. Similar to the "--follow" git diff
+ # argument. This would then cover cases where a rename occurs between
+ # the commit and its fixups, and allow fixups before the rename to also
+ # match. However, the former case is unexpected and the latter case
+ # would not be that useful.
+ if self._touches_path_basis(path, filter_status, check_dir):
+ return True
+ # Mark this as a valid target for the path if one of our fixups changes
+ # this path.
+ # NOTE: We use _touch_path_basis to prevent recursion. This means we
+ # will only check one layer up or down, but we only expect fixups of
+ # up to depth 1.
+ for fixup_target in self.fixups:
+ if fixup_target._touches_path_basis(path, filter_status, check_dir):
+ return True
+ # Mark this as a valid target if our target changes this path.
+ if self.target is not None and self.target._touches_path_basis(
+ path, filter_status, check_dir
+ ):
+ return True
+ return False
+
+ def _touches_path_basis(
+ self, path: str, filter_status: None | str, check_dir: bool
+ ) -> bool:
+ """
+ Whether this target touches the given path.
+ :param path: The path to check.
+ :param filter_status: Limit the detected changes to the given status.
+ :param check_dir: Whether we should treat `path` as a directory and check for
+ files within it.
+ :returns: Whether this target matches.
+ """
+ for file_change in self.changes:
+ if filter_status is not None and file_change.status not in filter_status:
+ continue
+ for test_path in (file_change.path, file_change.new_path):
+ if check_dir:
+ if os.path.commonpath((os.path.dirname(test_path), path)) == path:
+ # test_path's directory matches the path or is within it.
+ return True
+ elif test_path == path:
+ return True
+ return False
+
+
+def get_fixup_targets(
+ target_list: list[FixupTarget],
+ from_commit: str,
+ to_commit: str,
+ fixup_depth: int = 0,
+) -> None:
+ """
+ Find all the commits that can be targeted by a fixup between the given
+ commits.
+ :param target_list: The list to fill with targets. Appended in the order of
+ `from_commit` to `to_commit`.
+ :param from_commit: The commit to start from (non-inclusive).
+ :param to_commit: The commit to end on (inclusive).
+ :param fixup_depth: The maximum "depth" of fixups. I.e. how many "fixup!"
+ prefixes to allow.
+ """
+ raw_output = git_get(
+ [
+ "log",
+ "--pretty=format:%H,%h,%s",
+ "--reverse",
+ "--raw",
+ "-z",
+ f"{from_commit}..{to_commit}",
+ ],
+ strip=False,
+ )
+ pretty_regex = re.compile(
+ r"(?P<commit>[0-9a-f]+),(?P<short_ref>[0-9a-f]+),(?P<title>[^\n\0]*)\n"
+ )
+ excluded_regex_list = [
+ re.compile(r"^Bug [0-9]+.*r="), # Backported Mozilla bug.
+ re.compile(r"^dropme! "),
+ ]
+
+ while raw_output:
+ match = pretty_regex.match(raw_output)
+ if not match:
+ raise ValueError(f"Invalid pretty format: {raw_output[:100]}...")
+ fixup_target = FixupTarget(
+ match.group("commit"), match.group("short_ref"), match.group("title")
+ )
+ raw_output = raw_output[match.end() :]
+ while raw_output and raw_output[0] != "\0":
+ file_change, end = parse_raw_diff_line(raw_output)
+ fixup_target.changes.append(file_change)
+ raw_output = raw_output[end:]
+ if raw_output:
+ # Skip over the "\0".
+ raw_output = raw_output[1:]
+
+ for regex in excluded_regex_list:
+ if regex.match(fixup_target.title):
+ # Exclude from the list.
+ continue
+
+ trimmed_title, depth = fixup_target.trim_fixup()
+ if depth:
+ original_target = None
+ for target in target_list:
+ if target.title == trimmed_title:
+ original_target = target
+ break
+
+ if original_target:
+ original_target.fixups.append(fixup_target)
+ fixup_target.target = original_target
+ if depth > fixup_depth:
+ # Exclude from the list.
+ continue
+
+ target_list.append(fixup_target)
+
+
+class NewCommitBasis:
+ def __init__(self) -> None:
+ self.staged_paths: set[str] = set()
+ self.adding_paths: set[str] = set()
+
+ def add(self, paths: Iterable[str], staged: bool) -> None:
+ """
+ Add a path to include in this commit.
+ :param paths: The paths to add.
+ :param staged: Whether we are adding already staged changes.
+ """
+ if staged:
+ self.staged_paths.update(paths)
+ return
+
+ self.adding_paths.update(paths)
+
+
+class NewCommit(NewCommitBasis):
+ """Represents a new commit that we want to create."""
- def parse_log_line(line):
- [commit, short_ref, title] = line.split(",", 2)
- return {"commit": commit, "short-ref": short_ref, "title": title}
+ def __init__(self, alias: str) -> None:
+ """
+ :param alias: The alias name for the commit.
+ """
+ super().__init__()
+ self.alias = alias
- options = [
- parse_log_line(line)
- for line in git_get(
- [
- "log",
- "--pretty=format:%H,%h,%s",
- f"{firefox_commit}..HEAD",
- "--",
- filename,
- ]
+
+class NewFixup(NewCommitBasis):
+ """Represents a new fixup commit that we want to create."""
+
+ def __init__(self, target: FixupTarget) -> None:
+ """
+ :param target: The commit to target with the fixup.
+ """
+ super().__init__()
+ self.target = target
+
+
+def get_suggested_fixup_targets_for_change(
+ file_change: FileChange,
+ fixup_target_list: list[FixupTarget],
+ firefox_directories_lazy: Callable[[], set[str]],
+) -> Iterator[FixupTarget]:
+ """
+ Find the suggested fixup targets for the given file change.
+ :param file_change: The file change to get a suggestion for.
+ :param fixup_target_list: The list to choose from.
+ :param firefox_directories_lazy: Lazy method to return the firefox
+ directories.
+ :yield: The suggested fixup targets.
+ """
+
+ def filter_list(
+ path: str, filter_status: None | str = None, check_dir: bool = False
+ ) -> Iterator[FixupTarget]:
+ return (
+ t
+ for t in fixup_target_list
+ if t.touches_path(path, filter_status=filter_status, check_dir=check_dir)
)
+
+ if file_change.status == "D":
+ # Deleted.
+ # Find the commit that introduced this file or previously deleted it.
+ # I.e. added the file ("A"), renamed it ("R"), or deleted it ("D").
+ yield from filter_list(file_change.path, filter_status="ARD")
+ return
+
+ if file_change.status == "A":
+ # First check to see if this file name was actually touched before.
+ yielded_target = False
+ for target in filter_list(file_change.path):
+ yielded_target = True
+ yield target
+ if yielded_target:
+ return
+ # Else, find commits that introduced files in the same directory, or
+ # deleted in them, if they are not firefox directories.
+ dir_path = file_change.path
+ while True:
+ dir_path = os.path.dirname(dir_path)
+ if not dir_path or dir_path in firefox_directories_lazy():
+ return
+
+ yielded_target = False
+ for target in filter_list(dir_path, filter_status="ARD", check_dir=True):
+ yielded_target = True
+ yield target
+
+ if yielded_target:
+ return
+ # Else, search one directory higher.
+
+ if file_change.status == "R":
+ # Renamed.
+ # Find the commit that introduced the original name for this file.
+ yield from filter_list(file_change.path, filter_status="AR")
+ return
+
+ # Modified.
+ yield from filter_list(file_change.path)
+
+
+def ask_for_target(
+ file_change_list: list[FileChange],
+ new_commits_list: list[NewCommit | NewFixup],
+ suggested_fixup_target_list: list[FixupTarget],
+ full_fixup_target_list: list[FixupTarget],
+ staged: bool = False,
+) -> bool:
+ """
+ Ask the user to choose a target.
+ :param file_change_list: The file changes to ask for.
+ :param new_commits_list: The list of pending new commits, may be added to.
+ :param suggested_fixup_target_list: The list of suggested target fixups
+ to choose from.
+ :param staged: Whether this is for staged changes.
+ :returns: `True` if the operation should be aborted.
+ """
+
+ new_paths = [c.new_path for c in file_change_list]
+ all_paths = set(new_paths).union(c.path for c in file_change_list)
+ non_fixup_commits: list[NewCommit] = [
+ n for n in new_commits_list if isinstance(n, NewCommit)
]
- if not options:
- print(f"No commit found for {filename}")
- return None
- def valid_index(val):
+ shown_list: list[NewCommit | FixupTarget] = (
+ non_fixup_commits + suggested_fixup_target_list
+ )
+
+ can_skip = not staged
+ shown_full = False
+
+ index_offset = 2
+
+ def valid_response(val: str) -> tuple[str, None | NewCommit | FixupTarget]:
+ val = val.strip()
+
+ if val == "h":
+ return "help", None
+
+ if val == "a":
+ return "abort", None
+
if val == "d":
- return val
+ return "diff", None
+
+ if val == "f":
+ if shown_full:
+ # Already done once.
+ raise ValueError()
+ return "full-list", None
+ is_patch_full = val.startswith("P")
is_patch = val.startswith("p")
- if is_patch:
- val = val[1:]
+ if is_patch or is_patch_full:
+ index = int(val[1:], base=10) # Raises ValueError if not integer.
+ else:
+ index = int(val, base=10) # Raises ValueError if not integer.
+ if index == 0:
+ if not can_skip:
+ raise ValueError()
+ return "skip", None
+
+ if index == 1:
+ return "new", None
- # May raise a ValueError.
- as_index = int(val)
- if as_index < 0 or as_index > len(options):
+ index -= index_offset
+
+ if index < 0 or index >= len(shown_list):
raise ValueError()
- if as_index == 0:
- if is_patch:
+ selected = shown_list[index]
+
+ if is_patch_full:
+ return "patch-full", selected
+ if is_patch:
+ return "patch", selected
+ return "target", selected
+
+ def alias_response(val: str) -> str:
+ # Choose a default alias name if none is given.
+ val = val.strip() or f"New commit {len(non_fixup_commits)}"
+ for new_commit in non_fixup_commits:
+ if new_commit.alias == val:
+ # Already in use.
raise ValueError()
- return None
+ return val
+
+ def print_index_option(index: int, description: str) -> None:
+ print(f" \x1b[1m{index}\x1b[0m: {description}")
- return (is_patch, options[as_index - 1]["commit"])
+ def in_pink(text: str) -> str:
+ return f"\x1b[1;38;5;212m{text}\x1b[0m"
+ prefix_str = "For " + (in_pink("staged") if staged else "unstaged") + " changes to"
+ if len(new_paths) == 1:
+ print(f"{prefix_str} {in_pink(new_paths[0])}:")
+ else:
+ print(f"{prefix_str}:")
+ for path in new_paths:
+ print(f" {in_pink(path)}")
+ print("")
+
+ show_help = True
+ reshow_list = True
while True:
- print(f"For {filename}:\n")
- print(" \x1b[1m0\x1b[0m: None")
- for index, opt in enumerate(options):
- print(
- f" \x1b[1m{index + 1}\x1b[0m: "
- + f"\x1b[1;38;5;212m{opt['short-ref']}\x1b[0m "
- + opt["title"]
- )
+ if reshow_list:
+ if can_skip:
+ print_index_option(0, "Skip")
+ print_index_option(1, "New commit")
+ for index, target in enumerate(shown_list, start=index_offset):
+ if isinstance(target, NewCommit):
+ print_index_option(index, f"Add to new commit: {target.alias}")
+ else:
+ print_index_option(
+ index, f"Fixup: {in_pink(target.short_ref)} {target.title}"
+ )
+ reshow_list = False
print("")
- response = prompt_user(
- "Choose an <index> to fixup, or '0' to skip this file, "
- "or 'd' to view the pending diff, "
- "or 'p<index>' to view the patch for the index: ",
- valid_index,
+
+ response, selected = prompt_user(
+ (
+ "Choose an <index> to target. Type 'h' for additional options: "
+ if show_help
+ else "Choose an <index> to target or an option: "
+ ),
+ valid_response,
)
- if response is None:
- # Skip this file.
- return None
- if response == "d":
- git_run(["diff", "--", filename])
+ if response == "help":
+ print("Options:")
+ for option, desc in (
+ ("h", "show the available options."),
+ ("a", "abort this commit operation and all pending commits."),
+ (
+ ("", "")
+ if shown_full
+ else (
+ "f",
+ "show the full list of fixup targets, rather than just the suggested ones.",
+ )
+ ),
+ ("d", "view the diff for the pending file changes."),
+ (
+ "P<index>",
+ "view the patch for the index (including its relevant fixups).",
+ ),
+ (
+ "p<index>",
+ "view the patch for the index (including its relevant fixups), "
+ "limited to the current files.",
+ ),
+ ):
+ if not option:
+ # Skip this option.
+ continue
+ print(f" \x1b[1m{option[0]}\x1b[0m{option[1:].ljust(7)}: {desc}")
+ # Do not show the help option again.
+ show_help = False
+ continue
+
+ if response == "abort":
+ return True
+
+ if response == "skip":
+ return False
+
+ if response == "new":
+ new_alias = prompt_user(
+ "Enter an optional temporary alias for this new commit: ",
+ alias_response,
+ )
+ new_commit = NewCommit(new_alias)
+ new_commit.add(all_paths, staged)
+ new_commits_list.append(new_commit)
+ return False
+
+ if response == "target":
+ assert selected is not None
+
+ if isinstance(selected, NewCommit):
+ # Adding to a new commit.
+ selected.add(all_paths, staged)
+ return False
+
+ for new_fixup in new_commits_list:
+ if not isinstance(new_fixup, NewFixup):
+ continue
+ if new_fixup.target == selected:
+ # We already have a pending fixup commit that targets this
+ # selected target. Add this path to the same commit.
+ new_fixup.add(all_paths, staged)
+ return False
+
+ new_fixup = NewFixup(selected)
+ new_fixup.add(all_paths, staged)
+ new_commits_list.append(new_fixup)
+ return False
+
+ if response == "full-list":
+ shown_list = non_fixup_commits + full_fixup_target_list
+ shown_full = True
+ reshow_list = True
continue
- view_patch, commit = response
- if view_patch:
- git_run(["log", "-p", "-1", commit, "--", filename])
+ if response == "diff":
+ git_args = ["diff", "--color"]
+ if staged:
+ git_args.append("--staged")
+ git_args.extend(git_path_args(all_paths))
+ git_run_pager(git_args)
continue
- return commit
+ if response in ("patch", "patch-full"):
+ assert selected is not None
+
+ filter_paths = response == "patch"
+
+ if isinstance(selected, NewCommit):
+ git_sequence = [
+ ["diff", "--color", "--staged", *git_path_args((path,))]
+ for path in selected.staged_paths
+ if not filter_paths or path in all_paths
+ ]
+ git_sequence.extend(
+ ["diff", "--color", *git_path_args((path,))]
+ for path in selected.adding_paths
+ if not filter_paths or path in all_paths
+ )
+
+ # Show what the expected patch will be for the new commit.
+ git_run_pager(
+ arg_sequence=git_sequence, pager_prefix=f"{selected.alias}\n\n"
+ )
+ else:
+ # Show the log entry for the FixupTarget and each of its fixups.
+ # Order with the commmit closest to HEAD first. We expect
+ # selected.fixups to match this order.
+ git_sequence = []
+ # If `filter_paths` is set, we want to limit the log to the
+ # paths, and try to track any renames in the commit history.
+ prev_log_paths: None | set[str] = None
+ # For the first commit in the sequence, we use the old path
+ # names (rather than `c.new_path`) since we expect the commit
+ # which is closest to us to use the older names.
+ log_paths: None | set[str] = (
+ {c.path for c in file_change_list} if filter_paths else None
+ )
+ for target in (*selected.fixups, selected):
+ git_args = [
+ "log",
+ "--color",
+ "-p",
+ f"{target.commit}~1..{target.commit}",
+ ]
+ if filter_paths:
+ assert log_paths is not None
+ # Track the renamed paths.
+ prev_log_paths = log_paths.copy()
+ for file_change in target.changes:
+ if (
+ file_change.status == "R"
+ and file_change.new_path in log_paths
+ ):
+ # file was renamed in this change.
+ # Update log_paths to the new name.
+ # NOTE: This should have a similar effect to the
+ # --follow option for git log for a single file
+ # NOTE: File renames will not be properly
+ # tracked if a rename occurs outside of
+ # `selected.changes` or
+ # `selected.fixups[].changes`, but this is
+ # unexpected.
+ log_paths.remove(file_change.new_path)
+ log_paths.add(file_change.path)
+
+ # NOTE: This log entry may be empty if none of the paths
+ # match.
+ # NOTE: We include both log_paths and prev_log_paths to
+ # show renames in the diff output.
+ git_args.extend(git_path_args(log_paths | prev_log_paths))
+ git_sequence.append(git_args)
+ # Combine all the logs into one.
+ git_run_pager(arg_sequence=git_sequence)
+ continue
+
+ raise ValueError(f"Unexpected response: {response}")
-def auto_fixup(_args):
+def auto_commit(_args: argparse.Namespace) -> None:
"""
- Automatically find and fix up commits using the current unstaged changes.
+ Automatically find and fix up commits for any pending changes.
"""
+ # Want git log and add to be run from the root.
+ os.chdir(get_local_root())
# Only want to search as far back as the firefox commit.
firefox_commit = get_firefox_ref("HEAD").commit
- staged_files = get_changed_files("HEAD", staged=True)
- if staged_files:
- raise TbDevException(f"Have already staged files: {staged_files}")
+ staged_changes = [f for f in get_changed_files(staged=True)]
+ if staged_changes:
+ print("Existing staged changes for:")
+ for file_change in staged_changes:
+ print(f" {file_change.new_path}")
+ if not prompt_user(
+ "Include staged changes? (y/\x1b[4mn\x1b[0m)", binary_reply_default_no
+ ):
+ raise TbDevException("Cannot continue with pending staged changes")
+ print("")
- fixups = {}
- for filename in get_changed_files("HEAD"):
- commit = get_fixup_for_file(filename, firefox_commit)
- if commit is None:
+ full_target_list: list[FixupTarget] = []
+ # Determine if HEAD points to a branch or not and has an upstream commit.
+ # We choose check=False since the exit status is non-zero when we are in a
+ # detached state.
+ head_symbolic_ref = git_get(["symbolic-ref", "-q", "HEAD"], check=False)
+ if not head_symbolic_ref or not bool(
+ git_get(["for-each-ref", "--format=%(upstream)", head_symbolic_ref])
+ ):
+ # Unexpected, but not fatal.
+ print("HEAD has no upstream tracking!")
+ # Just include all commits since firefox_commit with no fixup depth
+ get_fixup_targets(full_target_list, firefox_commit, "HEAD", fixup_depth=0)
+ else:
+ upstream_commit = get_upstream_basis_commit("HEAD")
+ # Only include "fixup!" commits that are between here and the upstream
+ # tracking commit.
+ get_fixup_targets(
+ full_target_list, firefox_commit, upstream_commit, fixup_depth=0
+ )
+ get_fixup_targets(full_target_list, upstream_commit, "HEAD", fixup_depth=1)
+
+ # full_target_list is ordered with the earlier commits first. Reverse this.
+ full_target_list.reverse()
+ # Also reverse the fixups order to follow the same order.
+ for target in full_target_list:
+ target.fixups.reverse()
+
+ # Lazy load the list of firefox directories since they are unlikely to be
+ # needed.
+ @functools.cache
+ def firefox_directories_lazy() -> set[str]:
+ return {
+ dir_name
+ for dir_name in git_get(
+ [
+ "ls-tree",
+ "-r",
+ "-d",
+ "--name-only",
+ "--full-tree",
+ "-z",
+ firefox_commit,
+ ],
+ strip=False,
+ ).split("\0")
+ if dir_name
+ }
+
+ # Check untracked files to be added.
+ for path in git_get(
+ ["ls-files", "--other", "--exclude-standard", "-z"], strip=False
+ ).split("\0"):
+ if not path:
continue
- if commit not in fixups:
- fixups[commit] = [filename]
- else:
- fixups[commit].append(filename)
+ if prompt_user(
+ f"Start tracking file `{path}`? (y/\x1b[4mn\x1b[0m)",
+ binary_reply_default_no,
+ ):
+ # Include in the git diff output, but do not stage.
+ git_run(["add", "--intent-to-add", path])
print("")
- for commit, files in fixups.items():
- print("")
- git_run(["add", *files])
- git_run(["commit", f"--fixup={commit}"])
+ aborted = False
+ new_commits_list: list[NewCommit | NewFixup] = []
+ # First go through staged changes.
+ if staged_changes:
+ common_fixup_targets = None
+ for change in staged_changes:
+ target_iter = get_suggested_fixup_targets_for_change(
+ change, full_target_list, firefox_directories_lazy
+ )
+ if common_fixup_targets is None:
+ common_fixup_targets = set(target_iter)
+ else:
+ common_fixup_targets.intersection_update(target_iter)
+
+ assert common_fixup_targets is not None
+
+ aborted = ask_for_target(
+ staged_changes,
+ new_commits_list,
+ # Sort in the same order as full_target_list.
+ [target for target in full_target_list if target in common_fixup_targets],
+ full_target_list,
+ staged=True,
+ )
print("")
- if prompt_user(
- "Edit fixup commit message? (y/\x1b[4mn\x1b[0m)", binary_reply_default_no
- ):
+ if not aborted:
+ for file_change in get_changed_files():
+ target_list = list(
+ get_suggested_fixup_targets_for_change(
+ file_change, full_target_list, firefox_directories_lazy
+ )
+ )
+ aborted = ask_for_target(
+ [file_change],
+ new_commits_list,
+ target_list,
+ full_target_list,
+ staged=False,
+ )
+ print("")
+ if aborted:
+ break
+
+ if aborted:
+ return
+
+ # NOTE: Only the first commit can include staged changes.
+ # This should already be the case, but we want to double check.
+ for commit_index in range(1, len(new_commits_list)):
+ if new_commits_list[commit_index].staged_paths:
+ raise ValueError(f"Staged changes for commit {commit_index}")
+
+ for new_commit in new_commits_list:
+ print("")
+ if new_commit.adding_paths:
+ git_run(["add", *git_path_args(new_commit.adding_paths)])
+ if isinstance(new_commit, NewFixup):
+ git_run(["commit", f"--fixup={new_commit.target.commit}"])
+ print("")
+ is_double_fixup = bool(new_commit.target.target)
+ if not is_double_fixup and prompt_user(
+ "Edit fixup commit message? (y/\x1b[4mn\x1b[0m)",
+ binary_reply_default_no,
+ ):
+ git_run(["commit", "--amend"])
+ print("")
+ else:
+ git_run(["commit", "-m", new_commit.alias])
git_run(["commit", "--amend"])
+ print("")
-def clean_fixups(_args):
+def clean_fixups(_args: argparse.Namespace) -> None:
"""
Perform an interactive rebase that automatically applies fixups, similar to
--autosquash but also works on fixups of fixups.
"""
- user_editor = git_get(["var", "GIT_SEQUENCE_EDITOR"])[0]
+ user_editor = git_get(["var", "GIT_SEQUENCE_EDITOR"])
sub_editor = os.path.join(
os.path.dirname(os.path.realpath(__file__)), FIXUP_PREPROCESSOR_EDITOR
)
@@ -525,7 +1326,7 @@ def clean_fixups(_args):
)
-def show_default(_args):
+def show_default(_args: argparse.Namespace) -> None:
"""
Print the default branch name from gitlab.
"""
@@ -536,7 +1337,7 @@ def show_default(_args):
print(f"{upstream}/{default_branch}")
-def branch_from_default(args):
+def branch_from_default(args: argparse.Namespace) -> None:
"""
Fetch the default gitlab branch from upstream and create a new local branch.
"""
@@ -557,7 +1358,7 @@ def branch_from_default(args):
)
-def move_to_default(args):
+def move_to_default(args: argparse.Namespace) -> None:
"""
Fetch the default gitlab branch from upstream and move the specified
branch's commits on top. A new branch will be created tracking the default
@@ -569,7 +1370,7 @@ def move_to_default(args):
if branch_name is None:
# Use current branch as default.
try:
- branch_name = git_get(["branch", "--show-current"])[0]
+ branch_name = git_get(["branch", "--show-current"])
except IndexError:
raise TbDevException("No current branch")
@@ -608,7 +1409,7 @@ def move_to_default(args):
git_run(["cherry-pick", f"{current_basis}..{old_branch_name}"], check=False)
-def show_range_diff(args):
+def show_range_diff(args: argparse.Namespace) -> None:
"""
Show the range diff between two branches, from their firefox bases.
"""
@@ -624,21 +1425,21 @@ def show_range_diff(args):
)
-def show_diff_diff(args):
+def show_diff_diff(args: argparse.Namespace) -> None:
"""
Show the diff between the diffs of two branches, relative to their firefox
bases.
"""
- config_res = git_get(["config", "--get", "diff.tool"])
- if not config_res:
+ try:
+ diff_tool = next(git_lines(["config", "--get", "diff.tool"]))
+ except StopIteration:
raise TbDevException("No diff.tool configured for git")
- diff_tool = config_res[0]
# Filter out parts of the diff we expect to be different.
index_regex = re.compile(r"index [0-9a-f]{12}\.\.[0-9a-f]{12}")
lines_regex = re.compile(r"@@ -[0-9]+,[0-9]+ \+[0-9]+,[0-9]+ @@(?P<rest>.*)")
- def save_diff(branch):
+ def save_diff(branch: str) -> str:
firefox_commit = get_firefox_ref(branch).commit
file_desc, file_name = tempfile.mkstemp(
text=True, prefix=f'{branch.split("/")[-1]}-'
@@ -653,6 +1454,7 @@ def show_diff_diff(args):
)
with os.fdopen(file_desc, "w") as file:
+ assert diff_process.stdout is not None
for line in diff_process.stdout:
if index_regex.match(line):
# Fake data that will match.
@@ -665,7 +1467,7 @@ def show_diff_diff(args):
continue
file.write(line)
- status = diff_process.poll()
+ status = diff_process.wait()
if status != 0:
raise TbDevException(f"git diff exited with status {status}")
@@ -681,7 +1483,7 @@ def show_diff_diff(args):
# * -------------------- *
-def branch_complete(prefix, parsed_args, **kwargs):
+def branch_complete(prefix: str, **_kwargs: Any) -> list[str]:
"""
Complete the argument with a branch name.
"""
@@ -689,7 +1491,7 @@ def branch_complete(prefix, parsed_args, **kwargs):
return []
try:
branches = [ref.name for ref in get_refs("head", "")]
- branches.extend([ref.name for ref in get_refs("remote", "")])
+ branches.extend(ref.name for ref in get_refs("remote", ""))
branches.append("HEAD")
except Exception:
return []
@@ -699,7 +1501,20 @@ def branch_complete(prefix, parsed_args, **kwargs):
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(required=True)
-for name, details in {
+
+class ArgConfig(TypedDict):
+ help: str
+ metavar: NotRequired[str]
+ nargs: NotRequired[str]
+ completer: NotRequired[Callable[[str], list[str]]]
+
+
+class CommandConfig(TypedDict):
+ func: Callable[[argparse.Namespace], None]
+ args: NotRequired[dict[str, ArgConfig]]
+
+
+all_commands: dict[str, CommandConfig] = {
"show-upstream-basis-commit": {
"func": show_upstream_basis_commit,
},
@@ -716,8 +1531,8 @@ for name, details in {
},
},
},
- "auto-fixup": {
- "func": auto_fixup,
+ "auto-commit": {
+ "func": auto_commit,
},
"clean-fixups": {
"func": clean_fixups,
@@ -794,20 +1609,25 @@ for name, details in {
"regex": {"help": "the regex that the files must contain"},
},
},
-}.items():
- help_message = re.sub(r"\s+", " ", details["func"].__doc__).strip()
+}
+
+for name, command_config in all_commands.items():
+ help_message = command_config["func"].__doc__
+ assert isinstance(help_message, str)
+ help_message = re.sub(r"\s+", " ", help_message).strip()
sub = subparsers.add_parser(name, help=help_message)
- sub.set_defaults(func=details["func"])
- for arg, keywords in details.get("args", {}).items():
+ sub.set_defaults(func=command_config["func"])
+ for arg, keywords in command_config.get("args", {}).items():
completer = None
if "completer" in keywords:
completer = keywords["completer"]
del keywords["completer"]
sub_arg = sub.add_argument(arg, **keywords)
- if completer is not None:
- sub_arg.completer = completer
+ if completer is not None and argcomplete is not None:
+ sub_arg.completer = completer # type: ignore
-argcomplete.autocomplete(parser)
+if argcomplete is not None:
+ argcomplete.autocomplete(parser)
try:
if not within_browser_root():
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/f12904…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/f12904…
You're receiving this email because of your account on gitlab.torproject.org.
1
0
[Git][tpo/applications/tor-browser][tor-browser-146.0a1-16.0-2] 7 commits: fixup! BB 41803: Add some developer tools for working on tor-browser.
by henry (@henry) 11 Dec '25
by henry (@henry) 11 Dec '25
11 Dec '25
henry pushed to branch tor-browser-146.0a1-16.0-2 at The Tor Project / Applications / Tor Browser
Commits:
25553432 by Henry Wilkes at 2025-12-11T13:53:13+00:00
fixup! BB 41803: Add some developer tools for working on tor-browser.
TB 44367: Make git_get return the stdout string, rather than a list.
Add git_lines to generate lines.
- - - - -
523bc569 by Henry Wilkes at 2025-12-11T13:53:15+00:00
fixup! BB 41803: Add some developer tools for working on tor-browser.
TB 44367: Use raw diff to get list of file changes.
- - - - -
20c68b54 by Henry Wilkes at 2025-12-11T13:53:16+00:00
fixup! BB 41803: Add some developer tools for working on tor-browser.
TB 44367: Add type annotations and parameter documentation.
- - - - -
a0bff3f5 by Henry Wilkes at 2025-12-11T13:53:17+00:00
fixup! BB 41803: Add some developer tools for working on tor-browser.
TB 44367: Make the argcomplete module optional.
- - - - -
a64f9e20 by Henry Wilkes at 2025-12-11T13:53:18+00:00
fixup! BB 41803: Add some developer tools for working on tor-browser.
TB 44367: Use function caching instead of global variables.
- - - - -
80acc6a0 by Henry Wilkes at 2025-12-11T13:53:19+00:00
fixup! BB 41803: Add some developer tools for working on tor-browser.
TB 44367: Fetch FIREFOX_ tags from the remote if they are missing.
- - - - -
1b34bbe4 by Henry Wilkes at 2025-12-11T13:53:21+00:00
fixup! BB 41803: Add some developer tools for working on tor-browser.
TB 44367: Improve the auto-fixup/auto-commit command.
The auto-fixup command was renamed to auto-commit. It now also handles:
1. Already staged changes.
2. Untracked/added files.
3. Removed files.
4. Renamed files.
5. Allowing the user to create a new commit.
- - - - -
1 changed file:
- tools/base_browser/tb-dev
Changes:
=====================================
tools/base_browser/tb-dev
=====================================
@@ -6,6 +6,7 @@ Useful tools for working on tor-browser repository.
import argparse
import atexit
+import functools
import json
import os
import re
@@ -14,8 +15,15 @@ import sys
import tempfile
import termios
import urllib.request
+from collections.abc import Callable, Iterable, Iterator
+from types import ModuleType
+from typing import Any, NotRequired, TypedDict, TypeVar
-import argcomplete
+argcomplete: None | ModuleType = None
+try:
+ import argcomplete
+except ImportError:
+ pass
GIT_PATH = "/usr/bin/git"
UPSTREAM_URLS = {
@@ -36,9 +44,14 @@ class TbDevException(Exception):
pass
-def git_run(args, check=True, env=None):
+def git_run(
+ args: list[str], check: bool = True, env: None | dict[str, str] = None
+) -> None:
"""
Run a git command with output sent to stdout.
+ :param args: The arguments to pass to git.
+ :param check: Whether to check for success.
+ :param env: Optional environment to set.
"""
if env is not None:
tmp_env = dict(os.environ)
@@ -51,46 +64,122 @@ def git_run(args, check=True, env=None):
raise TbDevException(str(err)) from err
-def git_get(args):
+def git_run_pager(
+ args: list[str] | None = None,
+ arg_sequence: Iterable[list[str]] | None = None,
+ pager_prefix: None | str = None,
+) -> None:
"""
- Run a git command with each non-empty line returned in a list.
+ Run a sequence of git commands with the output concatenated and sent to the
+ git pager.
+ :param args: The arguments to pass to git, or `None` if a sequence is desired.
+ :param arg_sequence: A sequence representing several git commands.
+ :param pager_prefix: An optional text to send to the pager first.
+ """
+ if arg_sequence is None:
+ if args is not None:
+ arg_sequence = (args,)
+ else:
+ raise ValueError("Missing `arg_sequence` or `args`")
+ elif args is not None:
+ raise ValueError("Unexpected both args and arg_sequence")
+
+ pager = git_get(["var", "GIT_PAGER"])
+ if not pager:
+ raise TbDevException("Missing a GIT_PAGER")
+ command = [pager]
+ if os.path.basename(pager) == "less":
+ # Show colours.
+ command.append("-R")
+
+ pager_process = subprocess.Popen(command, stdin=subprocess.PIPE, text=True)
+ assert pager_process.stdin is not None
+
+ if pager_prefix is not None:
+ pager_process.stdin.write(pager_prefix)
+ pager_process.stdin.flush()
+
+ for git_args in arg_sequence:
+ subprocess.run(
+ [GIT_PATH, "--no-pager", *git_args], check=False, stdout=pager_process.stdin
+ )
+
+ pager_process.stdin.close()
+
+ status = pager_process.wait()
+ if status != 0:
+ raise TbDevException(f"git pager {pager} exited with status {status}")
+
+
+def git_get(args: list[str], strip: bool = True, check: bool = True) -> str:
+ """
+ Return the output from a git command.
+ :param args: The arguments to send to git.
+ :param strip: Whether to strip the whitespace from the output.
+ :param check: Whether to check for success.
+ :returns: The stdout.
"""
try:
git_process = subprocess.run(
- [GIT_PATH, *args], text=True, stdout=subprocess.PIPE, check=True
+ [GIT_PATH, *args], text=True, stdout=subprocess.PIPE, check=check
)
except subprocess.CalledProcessError as err:
raise TbDevException(str(err)) from err
- return [line for line in git_process.stdout.split("\n") if line]
+ ret = git_process.stdout
+ if strip:
+ ret = ret.strip()
+ return ret
-local_root = None
+def git_lines(args: list[str]) -> Iterator[str]:
+ """
+ Yields the non-empty lines returned by the git command.
+ :param args: The arguments to send to git.
+ :yield: The lines.
+ """
+ for line in git_get(args, strip=False).split("\n"):
+ if not line:
+ continue
+ yield line
+
+
+def git_path_args(path_iter: Iterable[str]) -> Iterator[str]:
+ """
+ Generate the trailing arguments to specify paths in git commands, includes
+ the "--" separator just before the paths.
+ :param path_iter: The paths that should be passed in.
+ :yields: The git arguments.
+ """
+ yield "--"
+ for path in path_iter:
+ yield f":(literal){path}"
-def get_local_root():
+(a)functools.cache
+def get_local_root() -> str:
"""
Get the path for the tor-browser root directory.
+ :returns: The local root.
"""
- global local_root
- if local_root is None:
- try:
- # Make sure we have a matching remote in this git repository.
- if get_upstream_details()["is-browser-repo"]:
- local_root = git_get(["rev-parse", "--show-toplevel"])[0]
- else:
- local_root = ""
- except TbDevException:
- local_root = ""
- return local_root
+ try:
+ # Make sure we have a matching remote in this git repository.
+ if get_upstream_details()["is-browser-repo"] == "True":
+ return git_get(["rev-parse", "--show-toplevel"])
+ else:
+ return ""
+ except TbDevException:
+ return ""
-def determine_upstream_details():
+(a)functools.cache
+def get_upstream_details() -> dict[str, str]:
"""
- Determine details about the upstream.
+ Get details about the upstream repository.
+ :returns: The details.
"""
remote_urls = {
- remote: git_get(["remote", "get-url", remote])[0]
- for remote in git_get(["remote"])
+ remote: git_get(["remote", "get-url", remote])
+ for remote in git_lines(["remote"])
}
matches = {
@@ -102,7 +191,7 @@ def determine_upstream_details():
}
is_browser_repo = len(matches) > 0
- details = {"is-browser-repo": is_browser_repo}
+ details = {"is-browser-repo": str(is_browser_repo)}
origin_remote_repo = matches.get("origin", None)
upstream_remote_repo = matches.get("upstream", None)
@@ -125,31 +214,30 @@ def determine_upstream_details():
return details
-cached_upstream_details = None
-
-
-def get_upstream_details():
- """
- Get details about the upstream repository.
- """
- global cached_upstream_details
- if cached_upstream_details is None:
- cached_upstream_details = determine_upstream_details()
- return cached_upstream_details
-
-
class Reference:
"""Represents a git reference to a commit."""
- def __init__(self, name, commit):
- self.name = name
+ _REFS_REGEX = re.compile(r"refs/[a-z]+/")
+
+ def __init__(self, full_name: str, commit: str) -> None:
+ """
+ :param full_name: The full reference name. E.g. "refs/tags/MyTag".
+ :param commit: The commit hash for the commit this reference points to.
+ """
+ match = self.__class__._REFS_REGEX.match(full_name)
+ if not match:
+ raise ValueError(f"Invalid reference name {full_name}")
+ self.full_name = full_name
+ self.name = full_name[match.end() :]
self.commit = commit
-def get_refs(ref_type, name_start):
+def get_refs(ref_type: str, name_start: str) -> Iterator[Reference]:
"""
- Get a list of references that match the given 'ref_type' ("tag" or "remote"
- or "head") that starts with the given 'name_start'.
+ Get a list of references that match the given conditions.
+ :param ref_type: The ref type to search for ("tag" or "remote" or "head").
+ :param name_start: The ref name start to match against.
+ :yield: The matching references.
"""
if ref_type == "tag":
ref_start = "refs/tags/"
@@ -163,56 +251,83 @@ def get_refs(ref_type, name_start):
fstring = "%(*objectname),%(objectname),%(refname)"
pattern = f"{ref_start}{name_start}**"
- def line_to_ref(line):
+ def line_to_ref(line: str) -> Reference:
[objectname_reference, objectname, ref_name] = line.split(",", 2)
# For annotated tags, the objectname_reference is non-empty and points
# to an actual commit.
# For remotes, heads and lightweight tags, the objectname_reference will
# be empty and objectname will point directly to the commit.
- return Reference(
- ref_name.replace(ref_start, "", 1), objectname_reference or objectname
- )
+ return Reference(ref_name, objectname_reference or objectname)
- return [
+ return (
line_to_ref(line)
- for line in git_get(["for-each-ref", f"--format={fstring}", pattern])
- ]
+ for line in git_lines(["for-each-ref", f"--format={fstring}", pattern])
+ )
-def get_nearest_ref(ref_type, name_start, search_from):
+def get_firefox_ref(search_from: str) -> Reference:
"""
- Search backwards from the 'search_from' commit to find the first commit
- that matches the given 'ref_type' that starts with the given 'name_start'.
+ Search for the commit that comes from firefox.
+ :param search_from: The commit to search backwards from.
+ :returns: The firefox reference.
"""
- ref_list = get_refs(ref_type, name_start)
+ # Only search a limited history that should include the FIREFOX_ tag.
+ search_commits = [c for c in git_lines(["rev-list", "-1000", search_from])]
+
+ firefox_tag_prefix = "FIREFOX_"
- for commit in git_get(["rev-list", "-1000", search_from]):
- for ref in ref_list:
+ existing_tags = list(get_refs("tag", firefox_tag_prefix))
+ for commit in search_commits:
+ for ref in existing_tags:
if commit == ref.commit:
return ref
- raise TbDevException(f"No {name_start} commit found in the last 1000 commits")
-
-
-def get_firefox_ref(search_from):
+ # Might just need to fetch tags from the remote.
+ upstream = get_upstream_details().get("remote", None)
+ if upstream:
+ remote_ref: None | Reference = None
+ search_index = len(search_commits)
+ # Search the remote for a tag that is in our history.
+ # We want to avoid triggering a long fetch, so we just want to grab the
+ # tag that already points to a commit in our history.
+ for line in git_lines(
+ ["ls-remote", upstream, f"refs/tags/{firefox_tag_prefix}*"]
+ ):
+ objectname, name = line.split("\t", 1)
+ for index in range(search_index):
+ if search_commits[index] == objectname:
+ # Remove trailing "^{}" for commits pointed to by
+ # annotated tags.
+ remote_ref = Reference(re.sub(r"\^\{\}$", "", name), objectname)
+ # Only continue to search for references that are even
+ # closer to `search_from`.
+ search_index = index
+ break
+ if remote_ref is not None:
+ # Get a local copy of just this tag.
+ git_run(["fetch", "--no-tags", upstream, "tag", remote_ref.name])
+ return ref
+
+ raise TbDevException("Unable to find FIREFOX_ tag")
+
+
+def get_upstream_tracking_branch(search_from: str) -> str:
"""
- Search backwards from the 'search_from' commit to find the commit that comes
- from firefox.
+ :param search_from: The commit reference.
+ :returns: The upstream branch reference name.
"""
- return get_nearest_ref("tag", "FIREFOX_", search_from)
-
-
-def get_upstream_tracking_branch(search_from):
- return git_get(["rev-parse", "--abbrev-ref", f"{search_from}@{{upstream}}"])[0]
+ return git_get(["rev-parse", "--abbrev-ref", f"{search_from}@{{upstream}}"])
-def get_upstream_basis_commit(search_from):
+def get_upstream_basis_commit(search_from: str) -> str:
"""
Get the first common ancestor of search_from that is also in its upstream
branch.
+ :param search_from: The commit reference.
+ :returns: The upstream commit hash.
"""
upstream_branch = get_upstream_tracking_branch(search_from)
- commit = git_get(["merge-base", search_from, upstream_branch])[0]
+ commit = git_get(["merge-base", search_from, upstream_branch])
# Verify that the upstream commit shares the same firefox basis. Otherwise,
# this would indicate that the upstream is on an early or later FIREFOX
# base.
@@ -226,26 +341,82 @@ def get_upstream_basis_commit(search_from):
return commit
-def get_changed_files(from_commit, staged=False):
+class FileChange:
+ """Represents a git change to a commit."""
+
+ def __init__(self, status: str, path: str, new_path: str) -> None:
+ """
+ :param status: The file change status used within git diff. E.g. "M" for
+ modified, or "D" for deleted.
+ :param path: The source file path.
+ :param new_path: The file path after the change.
+ """
+ self.status = status
+ self.path = path
+ self.new_path = new_path
+
+
+RAW_DIFF_PATH_PATTERN = r"(?P<path>[^\0]*)\0"
+RAW_DIFF_LINE_REGEX = re.compile(
+ r":[0-7]+ [0-7]+ [0-9a-f]+ [0-9a-f]+ (?P<status>[ADMTUXRC])[0-9]*\0"
+ + RAW_DIFF_PATH_PATTERN
+)
+RAW_DIFF_PATH_REGEX = re.compile(RAW_DIFF_PATH_PATTERN)
+
+
+def parse_raw_diff_line(raw_output: str) -> tuple[FileChange, int]:
"""
- Get a list of filenames relative to the current working directory that have
+ Parse the --raw diff output from git.
+ :param raw_output: The raw output.
+ :returns: The change for this line, and the offset for the end of the raw
+ diff line.
+ """
+ match = RAW_DIFF_LINE_REGEX.match(raw_output)
+ if not match:
+ raise ValueError(f"Invalid raw output: {raw_output[:50]}...")
+ path = os.path.relpath(os.path.join(get_local_root(), match.group("path")))
+ status = match.group("status")
+ if status in ("R", "C"):
+ match = RAW_DIFF_PATH_REGEX.match(raw_output, pos=match.end())
+ if not match:
+ raise ValueError(f"Invalid raw output for rename: {raw_output[:50]}...")
+ new_path = os.path.relpath(os.path.join(get_local_root(), match.group("path")))
+ else:
+ new_path = path
+
+ return FileChange(status, path, new_path), match.end()
+
+
+def get_changed_files(
+ from_commit: None | str = None, staged: bool = False
+) -> Iterator[FileChange]:
+ """
+ Get a list of file changes relative to the current working directory that have
been changed since 'from_commit' (non-inclusive).
+ :param from_commit: The commit to compare against, otherwise use the git
+ diff default.
+ :param staged: Whether to limit the diff to staged changes.
+ :yield: The file changes.
"""
- args = ["diff"]
+ args = ["diff", "-z", "--raw"]
if staged:
args.append("--staged")
- args.append("--name-only")
- args.append(from_commit)
- return [
- os.path.relpath(os.path.join(get_local_root(), filename))
- for filename in git_get(args)
- ]
+ if from_commit:
+ args.append(from_commit)
+ raw_output = git_get(args, strip=False)
+ while raw_output:
+ file_change, end = parse_raw_diff_line(raw_output)
+ yield file_change
+ raw_output = raw_output[end:]
-def file_contains(filename, regex):
+def file_contains(filename: str, regex: re.Pattern[str]) -> bool:
"""
Return whether the file is a utf-8 text file containing the regular
expression given by 'regex'.
+ :param filename: The file path.
+ :param regex: The pattern to search for.
+ :returns: Whether the pattern was matched.
"""
with open(filename, encoding="utf-8") as file:
try:
@@ -258,9 +429,10 @@ def file_contains(filename, regex):
return False
-def get_gitlab_default():
+def get_gitlab_default() -> str:
"""
Get the name of the default branch on gitlab.
+ :returns: The branch name.
"""
repo_name = get_upstream_details().get("repo-name", None)
if repo_name is None:
@@ -283,12 +455,14 @@ def get_gitlab_default():
)
with urllib.request.urlopen(gitlab_request, timeout=20) as response:
- return json.load(response)["data"]["project"]["repository"]["rootRef"]
+ default = json.load(response)["data"]["project"]["repository"]["rootRef"]
+ assert isinstance(default, str)
+ return default
-def within_browser_root():
+def within_browser_root() -> bool:
"""
- Whether we are with the tor browser root.
+ :returns: Whether we are with the tor browser root.
"""
root = get_local_root()
if not root:
@@ -301,24 +475,24 @@ def within_browser_root():
# * -------------------- *
-def show_firefox_commit(_args):
+def show_firefox_commit(_args: argparse.Namespace) -> None:
"""
Print the tag name and commit for the last firefox commit below the current
HEAD.
"""
ref = get_firefox_ref("HEAD")
- print(ref.name)
+ print(ref.full_name)
print(ref.commit)
-def show_upstream_basis_commit(_args):
+def show_upstream_basis_commit(_args: argparse.Namespace) -> None:
"""
Print the last upstream commit for the current HEAD.
"""
print(get_upstream_basis_commit("HEAD"))
-def show_log(args):
+def show_log(args: argparse.Namespace) -> None:
"""
Show the git log between the current HEAD and the last firefox commit.
"""
@@ -326,7 +500,7 @@ def show_log(args):
git_run(["log", f"{commit}..HEAD", *args.gitargs], check=False)
-def show_files_containing(args):
+def show_files_containing(args: argparse.Namespace) -> None:
"""
List all the files that that have been modified for tor browser, that also
contain a regular expression.
@@ -336,33 +510,32 @@ def show_files_containing(args):
except re.error as err:
raise TbDevException(f"{args.regex} is not a valid python regex") from err
- file_list = get_changed_files(get_firefox_ref("HEAD").commit)
-
- for filename in file_list:
- if not os.path.isfile(filename):
+ for file_change in get_changed_files(get_firefox_ref("HEAD").commit):
+ path = file_change.new_path
+ if not os.path.isfile(path):
# deleted ofile
continue
- if file_contains(filename, regex):
- print(filename)
+ if file_contains(path, regex):
+ print(path)
-def show_changed_files(_args):
+def show_changed_files(_args: argparse.Namespace) -> None:
"""
List all the files that have been modified relative to upstream.
"""
- for filename in get_changed_files(get_upstream_basis_commit("HEAD")):
- print(filename)
+ for file_change in get_changed_files(get_upstream_basis_commit("HEAD")):
+ print(file_change.new_path)
-def lint_changed_files(args):
+def lint_changed_files(args: argparse.Namespace) -> None:
"""
Lint all the files that have been modified relative to upstream.
"""
os.chdir(get_local_root())
file_list = [
- f
+ f.new_path
for f in get_changed_files(get_upstream_basis_commit("HEAD"))
- if os.path.isfile(f) # Not deleted
+ if os.path.isfile(f.new_path) # Not deleted
]
# We add --warnings since clang only reports whitespace issues as warnings.
subprocess.run(
@@ -371,10 +544,18 @@ def lint_changed_files(args):
)
-def prompt_user(prompt, convert):
+# TODO: replace with "prompt_user[T](..., T]) -> T" after python 3.12 is the
+# minimum mach version.
+T = TypeVar("T")
+
+
+def prompt_user(prompt: str, convert: Callable[[str], T]) -> T:
"""
- Ask the user for some input until the given converter returns without
- throwing a ValueError.
+ Ask the user for some input.
+ :param prompt: The prompt to show the user.
+ :param convert: A method to convert the response into a type. Should
+ throw `ValueError` if the user should be re-prompted for a valid input.
+ :returns: The first valid user response.
"""
while True:
# Flush out stdin.
@@ -388,8 +569,12 @@ def prompt_user(prompt, convert):
pass
-def binary_reply_default_no(value):
- """Process a 'y' or 'n' reply, defaulting to 'n' if empty."""
+def binary_reply_default_no(value: str) -> bool:
+ """
+ Process a 'y' or 'n' reply, defaulting to 'n' if empty.
+ :param value: The user input.
+ :returns: Whether the answer is yes.
+ """
if value == "":
return False
if value.lower() == "y":
@@ -399,121 +584,737 @@ def binary_reply_default_no(value):
raise ValueError()
-def get_fixup_for_file(filename, firefox_commit):
- """Find the commit the given file should fix up."""
+class FixupTarget:
+ """Represents a commit that can be targeted by a fixup."""
+
+ def __init__(self, commit: str, short_ref: str, title: str) -> None:
+ """
+ :param commit: The commit hash for the commit.
+ :param short_ref: The shortened commit hash for display.
+ :param title: The first line of the commit message.
+ """
+ self.commit = commit
+ self.short_ref = short_ref
+ self.title = title
+ self.changes: list[FileChange] = []
+ self.fixups: list[FixupTarget] = []
+ self.target: None | FixupTarget = None
+
+ _FIXUP_REGEX = re.compile(r"^fixup! +")
+
+ def trim_fixup(self) -> tuple[str, int]:
+ """
+ Trim the "fixup!" prefixes.
+ :returns: The stripped commit title and the fixup depth (how many fixups
+ prefixes there were).
+ """
+ title = self.title
+ depth = 0
+ while True:
+ match = self.__class__._FIXUP_REGEX.match(title)
+ if not match:
+ return title, depth
+ title = title[match.end() :]
+ depth += 1
+
+ def touches_path(
+ self, path: str, filter_status: None | str = None, check_dir: bool = False
+ ) -> bool:
+ """
+ Whether this target, or one of its fixups or target, touches the given
+ path.
+ :param path: The path to check.
+ :param filter_status: Limit the detected changes to the given status(es).
+ :param check_dir: Whether we should treat `path` as a directory and check for
+ files within it.
+ :returns: Whether this target matches.
+ """
+ # NOTE: In the case of renames, we generally assume that renames occur
+ # in the fixup targets. E.g. "Commit 1" creates the file "file.txt", and
+ # "fixup! Commit 1" renames it to "new.txt". In this case, if the
+ # FixupTarget for "Commit 1" is passed in "file.txt" it will match. And
+ # if it is passed in "new.txt" it will also match via the self.fixups
+ # field, which will include the "fixup! Commit 1" rename.
+ # But the "fixup ! Commit 1" FixupTargets will only match with
+ # "file.txt" if they occurred before the rename fixup, and will only
+ # match with "new.txt" if they occur after the rename fixup. With the
+ # exception of the rename fixup itself, which will match both.
+ #
+ # In principle, we could identify a file across renames (have a mapping
+ # from each commit to what the file is called at that stage) and match
+ # using this file identifier. Similar to the "--follow" git diff
+ # argument. This would then cover cases where a rename occurs between
+ # the commit and its fixups, and allow fixups before the rename to also
+ # match. However, the former case is unexpected and the latter case
+ # would not be that useful.
+ if self._touches_path_basis(path, filter_status, check_dir):
+ return True
+ # Mark this as a valid target for the path if one of our fixups changes
+ # this path.
+ # NOTE: We use _touch_path_basis to prevent recursion. This means we
+ # will only check one layer up or down, but we only expect fixups of
+ # up to depth 1.
+ for fixup_target in self.fixups:
+ if fixup_target._touches_path_basis(path, filter_status, check_dir):
+ return True
+ # Mark this as a valid target if our target changes this path.
+ if self.target is not None and self.target._touches_path_basis(
+ path, filter_status, check_dir
+ ):
+ return True
+ return False
+
+ def _touches_path_basis(
+ self, path: str, filter_status: None | str, check_dir: bool
+ ) -> bool:
+ """
+ Whether this target touches the given path.
+ :param path: The path to check.
+ :param filter_status: Limit the detected changes to the given status.
+ :param check_dir: Whether we should treat `path` as a directory and check for
+ files within it.
+ :returns: Whether this target matches.
+ """
+ for file_change in self.changes:
+ if filter_status is not None and file_change.status not in filter_status:
+ continue
+ for test_path in (file_change.path, file_change.new_path):
+ if check_dir:
+ if os.path.commonpath((os.path.dirname(test_path), path)) == path:
+ # test_path's directory matches the path or is within it.
+ return True
+ elif test_path == path:
+ return True
+ return False
+
+
+def get_fixup_targets(
+ target_list: list[FixupTarget],
+ from_commit: str,
+ to_commit: str,
+ fixup_depth: int = 0,
+) -> None:
+ """
+ Find all the commits that can be targeted by a fixup between the given
+ commits.
+ :param target_list: The list to fill with targets. Appended in the order of
+ `from_commit` to `to_commit`.
+ :param from_commit: The commit to start from (non-inclusive).
+ :param to_commit: The commit to end on (inclusive).
+ :param fixup_depth: The maximum "depth" of fixups. I.e. how many "fixup!"
+ prefixes to allow.
+ """
+ raw_output = git_get(
+ [
+ "log",
+ "--pretty=format:%H,%h,%s",
+ "--reverse",
+ "--raw",
+ "-z",
+ f"{from_commit}..{to_commit}",
+ ],
+ strip=False,
+ )
+ pretty_regex = re.compile(
+ r"(?P<commit>[0-9a-f]+),(?P<short_ref>[0-9a-f]+),(?P<title>[^\n\0]*)\n"
+ )
+ excluded_regex_list = [
+ re.compile(r"^Bug [0-9]+.*r="), # Backported Mozilla bug.
+ re.compile(r"^dropme! "),
+ ]
+
+ while raw_output:
+ match = pretty_regex.match(raw_output)
+ if not match:
+ raise ValueError(f"Invalid pretty format: {raw_output[:100]}...")
+ fixup_target = FixupTarget(
+ match.group("commit"), match.group("short_ref"), match.group("title")
+ )
+ raw_output = raw_output[match.end() :]
+ while raw_output and raw_output[0] != "\0":
+ file_change, end = parse_raw_diff_line(raw_output)
+ fixup_target.changes.append(file_change)
+ raw_output = raw_output[end:]
+ if raw_output:
+ # Skip over the "\0".
+ raw_output = raw_output[1:]
+
+ for regex in excluded_regex_list:
+ if regex.match(fixup_target.title):
+ # Exclude from the list.
+ continue
+
+ trimmed_title, depth = fixup_target.trim_fixup()
+ if depth:
+ original_target = None
+ for target in target_list:
+ if target.title == trimmed_title:
+ original_target = target
+ break
+
+ if original_target:
+ original_target.fixups.append(fixup_target)
+ fixup_target.target = original_target
+ if depth > fixup_depth:
+ # Exclude from the list.
+ continue
+
+ target_list.append(fixup_target)
+
+
+class NewCommitBasis:
+ def __init__(self) -> None:
+ self.staged_paths: set[str] = set()
+ self.adding_paths: set[str] = set()
+
+ def add(self, paths: Iterable[str], staged: bool) -> None:
+ """
+ Add a path to include in this commit.
+ :param paths: The paths to add.
+ :param staged: Whether we are adding already staged changes.
+ """
+ if staged:
+ self.staged_paths.update(paths)
+ return
+
+ self.adding_paths.update(paths)
+
+
+class NewCommit(NewCommitBasis):
+ """Represents a new commit that we want to create."""
- def parse_log_line(line):
- [commit, short_ref, title] = line.split(",", 2)
- return {"commit": commit, "short-ref": short_ref, "title": title}
+ def __init__(self, alias: str) -> None:
+ """
+ :param alias: The alias name for the commit.
+ """
+ super().__init__()
+ self.alias = alias
- options = [
- parse_log_line(line)
- for line in git_get(
- [
- "log",
- "--pretty=format:%H,%h,%s",
- f"{firefox_commit}..HEAD",
- "--",
- filename,
- ]
+
+class NewFixup(NewCommitBasis):
+ """Represents a new fixup commit that we want to create."""
+
+ def __init__(self, target: FixupTarget) -> None:
+ """
+ :param target: The commit to target with the fixup.
+ """
+ super().__init__()
+ self.target = target
+
+
+def get_suggested_fixup_targets_for_change(
+ file_change: FileChange,
+ fixup_target_list: list[FixupTarget],
+ firefox_directories_lazy: Callable[[], set[str]],
+) -> Iterator[FixupTarget]:
+ """
+ Find the suggested fixup targets for the given file change.
+ :param file_change: The file change to get a suggestion for.
+ :param fixup_target_list: The list to choose from.
+ :param firefox_directories_lazy: Lazy method to return the firefox
+ directories.
+ :yield: The suggested fixup targets.
+ """
+
+ def filter_list(
+ path: str, filter_status: None | str = None, check_dir: bool = False
+ ) -> Iterator[FixupTarget]:
+ return (
+ t
+ for t in fixup_target_list
+ if t.touches_path(path, filter_status=filter_status, check_dir=check_dir)
)
+
+ if file_change.status == "D":
+ # Deleted.
+ # Find the commit that introduced this file or previously deleted it.
+ # I.e. added the file ("A"), renamed it ("R"), or deleted it ("D").
+ yield from filter_list(file_change.path, filter_status="ARD")
+ return
+
+ if file_change.status == "A":
+ # First check to see if this file name was actually touched before.
+ yielded_target = False
+ for target in filter_list(file_change.path):
+ yielded_target = True
+ yield target
+ if yielded_target:
+ return
+ # Else, find commits that introduced files in the same directory, or
+ # deleted in them, if they are not firefox directories.
+ dir_path = file_change.path
+ while True:
+ dir_path = os.path.dirname(dir_path)
+ if not dir_path or dir_path in firefox_directories_lazy():
+ return
+
+ yielded_target = False
+ for target in filter_list(dir_path, filter_status="ARD", check_dir=True):
+ yielded_target = True
+ yield target
+
+ if yielded_target:
+ return
+ # Else, search one directory higher.
+
+ if file_change.status == "R":
+ # Renamed.
+ # Find the commit that introduced the original name for this file.
+ yield from filter_list(file_change.path, filter_status="AR")
+ return
+
+ # Modified.
+ yield from filter_list(file_change.path)
+
+
+def ask_for_target(
+ file_change_list: list[FileChange],
+ new_commits_list: list[NewCommit | NewFixup],
+ suggested_fixup_target_list: list[FixupTarget],
+ full_fixup_target_list: list[FixupTarget],
+ staged: bool = False,
+) -> bool:
+ """
+ Ask the user to choose a target.
+ :param file_change_list: The file changes to ask for.
+ :param new_commits_list: The list of pending new commits, may be added to.
+ :param suggested_fixup_target_list: The list of suggested target fixups
+ to choose from.
+ :param staged: Whether this is for staged changes.
+ :returns: `True` if the operation should be aborted.
+ """
+
+ new_paths = [c.new_path for c in file_change_list]
+ all_paths = set(new_paths).union(c.path for c in file_change_list)
+ non_fixup_commits: list[NewCommit] = [
+ n for n in new_commits_list if isinstance(n, NewCommit)
]
- if not options:
- print(f"No commit found for {filename}")
- return None
- def valid_index(val):
+ shown_list: list[NewCommit | FixupTarget] = (
+ non_fixup_commits + suggested_fixup_target_list
+ )
+
+ can_skip = not staged
+ shown_full = False
+
+ index_offset = 2
+
+ def valid_response(val: str) -> tuple[str, None | NewCommit | FixupTarget]:
+ val = val.strip()
+
+ if val == "h":
+ return "help", None
+
+ if val == "a":
+ return "abort", None
+
if val == "d":
- return val
+ return "diff", None
+
+ if val == "f":
+ if shown_full:
+ # Already done once.
+ raise ValueError()
+ return "full-list", None
+ is_patch_full = val.startswith("P")
is_patch = val.startswith("p")
- if is_patch:
- val = val[1:]
+ if is_patch or is_patch_full:
+ index = int(val[1:], base=10) # Raises ValueError if not integer.
+ else:
+ index = int(val, base=10) # Raises ValueError if not integer.
+ if index == 0:
+ if not can_skip:
+ raise ValueError()
+ return "skip", None
+
+ if index == 1:
+ return "new", None
- # May raise a ValueError.
- as_index = int(val)
- if as_index < 0 or as_index > len(options):
+ index -= index_offset
+
+ if index < 0 or index >= len(shown_list):
raise ValueError()
- if as_index == 0:
- if is_patch:
+ selected = shown_list[index]
+
+ if is_patch_full:
+ return "patch-full", selected
+ if is_patch:
+ return "patch", selected
+ return "target", selected
+
+ def alias_response(val: str) -> str:
+ # Choose a default alias name if none is given.
+ val = val.strip() or f"New commit {len(non_fixup_commits)}"
+ for new_commit in non_fixup_commits:
+ if new_commit.alias == val:
+ # Already in use.
raise ValueError()
- return None
+ return val
+
+ def print_index_option(index: int, description: str) -> None:
+ print(f" \x1b[1m{index}\x1b[0m: {description}")
- return (is_patch, options[as_index - 1]["commit"])
+ def in_pink(text: str) -> str:
+ return f"\x1b[1;38;5;212m{text}\x1b[0m"
+ prefix_str = "For " + (in_pink("staged") if staged else "unstaged") + " changes to"
+ if len(new_paths) == 1:
+ print(f"{prefix_str} {in_pink(new_paths[0])}:")
+ else:
+ print(f"{prefix_str}:")
+ for path in new_paths:
+ print(f" {in_pink(path)}")
+ print("")
+
+ show_help = True
+ reshow_list = True
while True:
- print(f"For {filename}:\n")
- print(" \x1b[1m0\x1b[0m: None")
- for index, opt in enumerate(options):
- print(
- f" \x1b[1m{index + 1}\x1b[0m: "
- + f"\x1b[1;38;5;212m{opt['short-ref']}\x1b[0m "
- + opt["title"]
- )
+ if reshow_list:
+ if can_skip:
+ print_index_option(0, "Skip")
+ print_index_option(1, "New commit")
+ for index, target in enumerate(shown_list, start=index_offset):
+ if isinstance(target, NewCommit):
+ print_index_option(index, f"Add to new commit: {target.alias}")
+ else:
+ print_index_option(
+ index, f"Fixup: {in_pink(target.short_ref)} {target.title}"
+ )
+ reshow_list = False
print("")
- response = prompt_user(
- "Choose an <index> to fixup, or '0' to skip this file, "
- "or 'd' to view the pending diff, "
- "or 'p<index>' to view the patch for the index: ",
- valid_index,
+
+ response, selected = prompt_user(
+ (
+ "Choose an <index> to target. Type 'h' for additional options: "
+ if show_help
+ else "Choose an <index> to target or an option: "
+ ),
+ valid_response,
)
- if response is None:
- # Skip this file.
- return None
- if response == "d":
- git_run(["diff", "--", filename])
+ if response == "help":
+ print("Options:")
+ for option, desc in (
+ ("h", "show the available options."),
+ ("a", "abort this commit operation and all pending commits."),
+ (
+ ("", "")
+ if shown_full
+ else (
+ "f",
+ "show the full list of fixup targets, rather than just the suggested ones.",
+ )
+ ),
+ ("d", "view the diff for the pending file changes."),
+ (
+ "P<index>",
+ "view the patch for the index (including its relevant fixups).",
+ ),
+ (
+ "p<index>",
+ "view the patch for the index (including its relevant fixups), "
+ "limited to the current files.",
+ ),
+ ):
+ if not option:
+ # Skip this option.
+ continue
+ print(f" \x1b[1m{option[0]}\x1b[0m{option[1:].ljust(7)}: {desc}")
+ # Do not show the help option again.
+ show_help = False
+ continue
+
+ if response == "abort":
+ return True
+
+ if response == "skip":
+ return False
+
+ if response == "new":
+ new_alias = prompt_user(
+ "Enter an optional temporary alias for this new commit: ",
+ alias_response,
+ )
+ new_commit = NewCommit(new_alias)
+ new_commit.add(all_paths, staged)
+ new_commits_list.append(new_commit)
+ return False
+
+ if response == "target":
+ assert selected is not None
+
+ if isinstance(selected, NewCommit):
+ # Adding to a new commit.
+ selected.add(all_paths, staged)
+ return False
+
+ for new_fixup in new_commits_list:
+ if not isinstance(new_fixup, NewFixup):
+ continue
+ if new_fixup.target == selected:
+ # We already have a pending fixup commit that targets this
+ # selected target. Add this path to the same commit.
+ new_fixup.add(all_paths, staged)
+ return False
+
+ new_fixup = NewFixup(selected)
+ new_fixup.add(all_paths, staged)
+ new_commits_list.append(new_fixup)
+ return False
+
+ if response == "full-list":
+ shown_list = non_fixup_commits + full_fixup_target_list
+ shown_full = True
+ reshow_list = True
continue
- view_patch, commit = response
- if view_patch:
- git_run(["log", "-p", "-1", commit, "--", filename])
+ if response == "diff":
+ git_args = ["diff", "--color"]
+ if staged:
+ git_args.append("--staged")
+ git_args.extend(git_path_args(all_paths))
+ git_run_pager(git_args)
continue
- return commit
+ if response in ("patch", "patch-full"):
+ assert selected is not None
+
+ filter_paths = response == "patch"
+
+ if isinstance(selected, NewCommit):
+ git_sequence = [
+ ["diff", "--color", "--staged", *git_path_args((path,))]
+ for path in selected.staged_paths
+ if not filter_paths or path in all_paths
+ ]
+ git_sequence.extend(
+ ["diff", "--color", *git_path_args((path,))]
+ for path in selected.adding_paths
+ if not filter_paths or path in all_paths
+ )
+
+ # Show what the expected patch will be for the new commit.
+ git_run_pager(
+ arg_sequence=git_sequence, pager_prefix=f"{selected.alias}\n\n"
+ )
+ else:
+ # Show the log entry for the FixupTarget and each of its fixups.
+ # Order with the commmit closest to HEAD first. We expect
+ # selected.fixups to match this order.
+ git_sequence = []
+ # If `filter_paths` is set, we want to limit the log to the
+ # paths, and try to track any renames in the commit history.
+ prev_log_paths: None | set[str] = None
+ # For the first commit in the sequence, we use the old path
+ # names (rather than `c.new_path`) since we expect the commit
+ # which is closest to us to use the older names.
+ log_paths: None | set[str] = (
+ {c.path for c in file_change_list} if filter_paths else None
+ )
+ for target in (*selected.fixups, selected):
+ git_args = [
+ "log",
+ "--color",
+ "-p",
+ f"{target.commit}~1..{target.commit}",
+ ]
+ if filter_paths:
+ assert log_paths is not None
+ # Track the renamed paths.
+ prev_log_paths = log_paths.copy()
+ for file_change in target.changes:
+ if (
+ file_change.status == "R"
+ and file_change.new_path in log_paths
+ ):
+ # file was renamed in this change.
+ # Update log_paths to the new name.
+ # NOTE: This should have a similar effect to the
+ # --follow option for git log for a single file
+ # NOTE: File renames will not be properly
+ # tracked if a rename occurs outside of
+ # `selected.changes` or
+ # `selected.fixups[].changes`, but this is
+ # unexpected.
+ log_paths.remove(file_change.new_path)
+ log_paths.add(file_change.path)
+
+ # NOTE: This log entry may be empty if none of the paths
+ # match.
+ # NOTE: We include both log_paths and prev_log_paths to
+ # show renames in the diff output.
+ git_args.extend(git_path_args(log_paths | prev_log_paths))
+ git_sequence.append(git_args)
+ # Combine all the logs into one.
+ git_run_pager(arg_sequence=git_sequence)
+ continue
+
+ raise ValueError(f"Unexpected response: {response}")
-def auto_fixup(_args):
+def auto_commit(_args: argparse.Namespace) -> None:
"""
- Automatically find and fix up commits using the current unstaged changes.
+ Automatically find and fix up commits for any pending changes.
"""
+ # Want git log and add to be run from the root.
+ os.chdir(get_local_root())
# Only want to search as far back as the firefox commit.
firefox_commit = get_firefox_ref("HEAD").commit
- staged_files = get_changed_files("HEAD", staged=True)
- if staged_files:
- raise TbDevException(f"Have already staged files: {staged_files}")
+ staged_changes = [f for f in get_changed_files(staged=True)]
+ if staged_changes:
+ print("Existing staged changes for:")
+ for file_change in staged_changes:
+ print(f" {file_change.new_path}")
+ if not prompt_user(
+ "Include staged changes? (y/\x1b[4mn\x1b[0m)", binary_reply_default_no
+ ):
+ raise TbDevException("Cannot continue with pending staged changes")
+ print("")
- fixups = {}
- for filename in get_changed_files("HEAD"):
- commit = get_fixup_for_file(filename, firefox_commit)
- if commit is None:
+ full_target_list: list[FixupTarget] = []
+ # Determine if HEAD points to a branch or not and has an upstream commit.
+ # We choose check=False since the exit status is non-zero when we are in a
+ # detached state.
+ head_symbolic_ref = git_get(["symbolic-ref", "-q", "HEAD"], check=False)
+ if not head_symbolic_ref or not bool(
+ git_get(["for-each-ref", "--format=%(upstream)", head_symbolic_ref])
+ ):
+ # Unexpected, but not fatal.
+ print("HEAD has no upstream tracking!")
+ # Just include all commits since firefox_commit with no fixup depth
+ get_fixup_targets(full_target_list, firefox_commit, "HEAD", fixup_depth=0)
+ else:
+ upstream_commit = get_upstream_basis_commit("HEAD")
+ # Only include "fixup!" commits that are between here and the upstream
+ # tracking commit.
+ get_fixup_targets(
+ full_target_list, firefox_commit, upstream_commit, fixup_depth=0
+ )
+ get_fixup_targets(full_target_list, upstream_commit, "HEAD", fixup_depth=1)
+
+ # full_target_list is ordered with the earlier commits first. Reverse this.
+ full_target_list.reverse()
+ # Also reverse the fixups order to follow the same order.
+ for target in full_target_list:
+ target.fixups.reverse()
+
+ # Lazy load the list of firefox directories since they are unlikely to be
+ # needed.
+ @functools.cache
+ def firefox_directories_lazy() -> set[str]:
+ return {
+ dir_name
+ for dir_name in git_get(
+ [
+ "ls-tree",
+ "-r",
+ "-d",
+ "--name-only",
+ "--full-tree",
+ "-z",
+ firefox_commit,
+ ],
+ strip=False,
+ ).split("\0")
+ if dir_name
+ }
+
+ # Check untracked files to be added.
+ for path in git_get(
+ ["ls-files", "--other", "--exclude-standard", "-z"], strip=False
+ ).split("\0"):
+ if not path:
continue
- if commit not in fixups:
- fixups[commit] = [filename]
- else:
- fixups[commit].append(filename)
+ if prompt_user(
+ f"Start tracking file `{path}`? (y/\x1b[4mn\x1b[0m)",
+ binary_reply_default_no,
+ ):
+ # Include in the git diff output, but do not stage.
+ git_run(["add", "--intent-to-add", path])
print("")
- for commit, files in fixups.items():
- print("")
- git_run(["add", *files])
- git_run(["commit", f"--fixup={commit}"])
+ aborted = False
+ new_commits_list: list[NewCommit | NewFixup] = []
+ # First go through staged changes.
+ if staged_changes:
+ common_fixup_targets = None
+ for change in staged_changes:
+ target_iter = get_suggested_fixup_targets_for_change(
+ change, full_target_list, firefox_directories_lazy
+ )
+ if common_fixup_targets is None:
+ common_fixup_targets = set(target_iter)
+ else:
+ common_fixup_targets.intersection_update(target_iter)
+
+ assert common_fixup_targets is not None
+
+ aborted = ask_for_target(
+ staged_changes,
+ new_commits_list,
+ # Sort in the same order as full_target_list.
+ [target for target in full_target_list if target in common_fixup_targets],
+ full_target_list,
+ staged=True,
+ )
print("")
- if prompt_user(
- "Edit fixup commit message? (y/\x1b[4mn\x1b[0m)", binary_reply_default_no
- ):
+ if not aborted:
+ for file_change in get_changed_files():
+ target_list = list(
+ get_suggested_fixup_targets_for_change(
+ file_change, full_target_list, firefox_directories_lazy
+ )
+ )
+ aborted = ask_for_target(
+ [file_change],
+ new_commits_list,
+ target_list,
+ full_target_list,
+ staged=False,
+ )
+ print("")
+ if aborted:
+ break
+
+ if aborted:
+ return
+
+ # NOTE: Only the first commit can include staged changes.
+ # This should already be the case, but we want to double check.
+ for commit_index in range(1, len(new_commits_list)):
+ if new_commits_list[commit_index].staged_paths:
+ raise ValueError(f"Staged changes for commit {commit_index}")
+
+ for new_commit in new_commits_list:
+ print("")
+ if new_commit.adding_paths:
+ git_run(["add", *git_path_args(new_commit.adding_paths)])
+ if isinstance(new_commit, NewFixup):
+ git_run(["commit", f"--fixup={new_commit.target.commit}"])
+ print("")
+ is_double_fixup = bool(new_commit.target.target)
+ if not is_double_fixup and prompt_user(
+ "Edit fixup commit message? (y/\x1b[4mn\x1b[0m)",
+ binary_reply_default_no,
+ ):
+ git_run(["commit", "--amend"])
+ print("")
+ else:
+ git_run(["commit", "-m", new_commit.alias])
git_run(["commit", "--amend"])
+ print("")
-def clean_fixups(_args):
+def clean_fixups(_args: argparse.Namespace) -> None:
"""
Perform an interactive rebase that automatically applies fixups, similar to
--autosquash but also works on fixups of fixups.
"""
- user_editor = git_get(["var", "GIT_SEQUENCE_EDITOR"])[0]
+ user_editor = git_get(["var", "GIT_SEQUENCE_EDITOR"])
sub_editor = os.path.join(
os.path.dirname(os.path.realpath(__file__)), FIXUP_PREPROCESSOR_EDITOR
)
@@ -525,7 +1326,7 @@ def clean_fixups(_args):
)
-def show_default(_args):
+def show_default(_args: argparse.Namespace) -> None:
"""
Print the default branch name from gitlab.
"""
@@ -536,7 +1337,7 @@ def show_default(_args):
print(f"{upstream}/{default_branch}")
-def branch_from_default(args):
+def branch_from_default(args: argparse.Namespace) -> None:
"""
Fetch the default gitlab branch from upstream and create a new local branch.
"""
@@ -557,7 +1358,7 @@ def branch_from_default(args):
)
-def move_to_default(args):
+def move_to_default(args: argparse.Namespace) -> None:
"""
Fetch the default gitlab branch from upstream and move the specified
branch's commits on top. A new branch will be created tracking the default
@@ -569,7 +1370,7 @@ def move_to_default(args):
if branch_name is None:
# Use current branch as default.
try:
- branch_name = git_get(["branch", "--show-current"])[0]
+ branch_name = git_get(["branch", "--show-current"])
except IndexError:
raise TbDevException("No current branch")
@@ -608,7 +1409,7 @@ def move_to_default(args):
git_run(["cherry-pick", f"{current_basis}..{old_branch_name}"], check=False)
-def show_range_diff(args):
+def show_range_diff(args: argparse.Namespace) -> None:
"""
Show the range diff between two branches, from their firefox bases.
"""
@@ -624,21 +1425,21 @@ def show_range_diff(args):
)
-def show_diff_diff(args):
+def show_diff_diff(args: argparse.Namespace) -> None:
"""
Show the diff between the diffs of two branches, relative to their firefox
bases.
"""
- config_res = git_get(["config", "--get", "diff.tool"])
- if not config_res:
+ try:
+ diff_tool = next(git_lines(["config", "--get", "diff.tool"]))
+ except StopIteration:
raise TbDevException("No diff.tool configured for git")
- diff_tool = config_res[0]
# Filter out parts of the diff we expect to be different.
index_regex = re.compile(r"index [0-9a-f]{12}\.\.[0-9a-f]{12}")
lines_regex = re.compile(r"@@ -[0-9]+,[0-9]+ \+[0-9]+,[0-9]+ @@(?P<rest>.*)")
- def save_diff(branch):
+ def save_diff(branch: str) -> str:
firefox_commit = get_firefox_ref(branch).commit
file_desc, file_name = tempfile.mkstemp(
text=True, prefix=f'{branch.split("/")[-1]}-'
@@ -653,6 +1454,7 @@ def show_diff_diff(args):
)
with os.fdopen(file_desc, "w") as file:
+ assert diff_process.stdout is not None
for line in diff_process.stdout:
if index_regex.match(line):
# Fake data that will match.
@@ -665,7 +1467,7 @@ def show_diff_diff(args):
continue
file.write(line)
- status = diff_process.poll()
+ status = diff_process.wait()
if status != 0:
raise TbDevException(f"git diff exited with status {status}")
@@ -681,7 +1483,7 @@ def show_diff_diff(args):
# * -------------------- *
-def branch_complete(prefix, parsed_args, **kwargs):
+def branch_complete(prefix: str, **_kwargs: Any) -> list[str]:
"""
Complete the argument with a branch name.
"""
@@ -689,7 +1491,7 @@ def branch_complete(prefix, parsed_args, **kwargs):
return []
try:
branches = [ref.name for ref in get_refs("head", "")]
- branches.extend([ref.name for ref in get_refs("remote", "")])
+ branches.extend(ref.name for ref in get_refs("remote", ""))
branches.append("HEAD")
except Exception:
return []
@@ -699,7 +1501,20 @@ def branch_complete(prefix, parsed_args, **kwargs):
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(required=True)
-for name, details in {
+
+class ArgConfig(TypedDict):
+ help: str
+ metavar: NotRequired[str]
+ nargs: NotRequired[str]
+ completer: NotRequired[Callable[[str], list[str]]]
+
+
+class CommandConfig(TypedDict):
+ func: Callable[[argparse.Namespace], None]
+ args: NotRequired[dict[str, ArgConfig]]
+
+
+all_commands: dict[str, CommandConfig] = {
"show-upstream-basis-commit": {
"func": show_upstream_basis_commit,
},
@@ -716,8 +1531,8 @@ for name, details in {
},
},
},
- "auto-fixup": {
- "func": auto_fixup,
+ "auto-commit": {
+ "func": auto_commit,
},
"clean-fixups": {
"func": clean_fixups,
@@ -794,20 +1609,25 @@ for name, details in {
"regex": {"help": "the regex that the files must contain"},
},
},
-}.items():
- help_message = re.sub(r"\s+", " ", details["func"].__doc__).strip()
+}
+
+for name, command_config in all_commands.items():
+ help_message = command_config["func"].__doc__
+ assert isinstance(help_message, str)
+ help_message = re.sub(r"\s+", " ", help_message).strip()
sub = subparsers.add_parser(name, help=help_message)
- sub.set_defaults(func=details["func"])
- for arg, keywords in details.get("args", {}).items():
+ sub.set_defaults(func=command_config["func"])
+ for arg, keywords in command_config.get("args", {}).items():
completer = None
if "completer" in keywords:
completer = keywords["completer"]
del keywords["completer"]
sub_arg = sub.add_argument(arg, **keywords)
- if completer is not None:
- sub_arg.completer = completer
+ if completer is not None and argcomplete is not None:
+ sub_arg.completer = completer # type: ignore
-argcomplete.autocomplete(parser)
+if argcomplete is not None:
+ argcomplete.autocomplete(parser)
try:
if not within_browser_root():
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/9596a5…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/9596a5…
You're receiving this email because of your account on gitlab.torproject.org.
1
0
[Git][tpo/applications/mullvad-browser][mullvad-browser-146.0a1-16.0-2] MB 80: Enable Mullvad Browser as a default browser
by Pier Angelo Vendrame (@pierov) 11 Dec '25
by Pier Angelo Vendrame (@pierov) 11 Dec '25
11 Dec '25
Pier Angelo Vendrame pushed to branch mullvad-browser-146.0a1-16.0-2 at The Tor Project / Applications / Mullvad Browser
Commits:
0801264b by Pier Angelo Vendrame at 2025-12-11T12:21:20+01:00
MB 80: Enable Mullvad Browser as a default browser
- - - - -
10 changed files:
- browser/components/shell/ShellService.sys.mjs
- browser/components/shell/WindowsDefaultBrowser.cpp
- browser/components/shell/nsWindowsShellService.cpp
- + other-licenses/nsis/Contrib/ApplicationID/Makefile
- other-licenses/nsis/Contrib/ApplicationID/Set.cpp
- + other-licenses/nsis/Contrib/CityHash/Makefile
- toolkit/mozapps/defaultagent/EventLog.h
- toolkit/mozapps/defaultagent/SetDefaultBrowser.cpp
- widget/windows/WinTaskbar.cpp
- widget/windows/moz.build
Changes:
=====================================
browser/components/shell/ShellService.sys.mjs
=====================================
@@ -286,7 +286,7 @@ let ShellServiceInternal = {
) {
if (this._shouldSetDefaultPDFHandler()) {
lazy.log.info("Setting Firefox as default PDF handler");
- extraFileExtensions.push(".pdf", "FirefoxPDF");
+ extraFileExtensions.push(".pdf", "MullvadBrowserPDF");
} else {
lazy.log.info("Not setting Firefox as default PDF handler");
}
@@ -324,7 +324,7 @@ let ShellServiceInternal = {
try {
this.defaultAgent.setDefaultExtensionHandlersUserChoice(aumi, [
".pdf",
- "FirefoxPDF",
+ "MullvadBrowserPDF",
]);
} catch (err) {
telemetryResult = "ErrOther";
=====================================
browser/components/shell/WindowsDefaultBrowser.cpp
=====================================
@@ -24,7 +24,7 @@
#include <wchar.h>
#include <windows.h>
-#define APP_REG_NAME_BASE L"Firefox-"
+#define APP_REG_NAME_BASE L"MullvadBrowser-"
static bool IsWindowsLogonConnected() {
WCHAR userName[UNLEN + 1];
=====================================
browser/components/shell/nsWindowsShellService.cpp
=====================================
@@ -361,10 +361,12 @@ nsWindowsShellService::CheckAllProgIDsExist(bool* aResult) {
*aResult = result;
} else {
- *aResult =
- CheckProgIDExists(FormatProgID(L"FirefoxURL", aumid.get()).get()) &&
- CheckProgIDExists(FormatProgID(L"FirefoxHTML", aumid.get()).get()) &&
- CheckProgIDExists(FormatProgID(L"FirefoxPDF", aumid.get()).get());
+ *aResult = CheckProgIDExists(
+ FormatProgID(L"MullvadBrowserURL", aumid.get()).get()) &&
+ CheckProgIDExists(
+ FormatProgID(L"MullvadBrowserHTML", aumid.get()).get()) &&
+ CheckProgIDExists(
+ FormatProgID(L"MullvadBrowserPDF", aumid.get()).get());
}
return NS_OK;
=====================================
other-licenses/nsis/Contrib/ApplicationID/Makefile
=====================================
@@ -0,0 +1,14 @@
+CXXFLAGS=-Icityhash -DWIN32 -DNDEBUG -D_WINDOWS -D_USRDLL -DCITYHASH_EXPORTS -DUNICODE
+SRCS=Set.cpp
+OBJS=$(subst .cpp,.o,$(SRCS))
+LDFLAGS=-lole32 -lshlwapi -shared -Wl,--no-insert-timestamp
+PLUGIN=ApplicationID.dll
+
+all: $(PLUGIN)
+
+$(PLUGIN): $(OBJS)
+ $(CXX) $(OBJS) $(LDFLAGS) -o $@
+ llvm-strip $@
+
+clean:
+ $(RM) $(OBJS) $(PLUGIN)
=====================================
other-licenses/nsis/Contrib/ApplicationID/Set.cpp
=====================================
@@ -35,7 +35,8 @@ unsigned int g_stringsize;
TCHAR *g_variables;
// Indicates that an application supports dual desktop and immersive modes. In Windows 8, this property is only applicable for web browsers.
-DEFINE_PROPERTYKEY(PKEY_AppUserModel_IsDualMode, 0x9F4C2855, 0x9F79, 0x4B39, 0xA8, 0xD0, 0xE1, 0xD4, 0x2D, 0xE1, 0xD5, 0xF3, 11);
+// mingw already defines this in propkey.h.
+// DEFINE_PROPERTYKEY(PKEY_AppUserModel_IsDualMode, 0x9F4C2855, 0x9F79, 0x4B39, 0xA8, 0xD0, 0xE1, 0xD4, 0x2D, 0xE1, 0xD5, 0xF3, 11);
int popstring(TCHAR *str, int len);
void pushstring(const TCHAR *str, int len);
=====================================
other-licenses/nsis/Contrib/CityHash/Makefile
=====================================
@@ -0,0 +1,14 @@
+CXXFLAGS=-Icityhash -DWIN32 -DNDEBUG -D_WINDOWS -D_USRDLL -DCITYHASH_EXPORTS -DUNICODE
+SRCS=CityHash.cpp cityhash/city.cpp
+OBJS=$(subst .cpp,.o,$(SRCS))
+LDFLAGS=-shared -Wl,--no-insert-timestamp
+PLUGIN=CityHash.dll
+
+all: $(PLUGIN)
+
+$(PLUGIN): $(OBJS)
+ $(CXX) $(OBJS) $(LDFLAGS) -o $@
+ llvm-strip $@
+
+clean:
+ $(RM) $(OBJS) $(PLUGIN)
=====================================
toolkit/mozapps/defaultagent/EventLog.h
=====================================
@@ -7,7 +7,7 @@
#ifndef __DEFAULT_BROWSER_AGENT_EVENT_LOG_H__
#define __DEFAULT_BROWSER_AGENT_EVENT_LOG_H__
-#include "mozilla/Types.h"
+#include <cwchar>
MOZ_BEGIN_EXTERN_C
@@ -15,10 +15,21 @@ extern MOZ_EXPORT const wchar_t* gWinEventLogSourceName;
MOZ_END_EXTERN_C
-#include "mozilla/WindowsEventLog.h"
-
-#define LOG_ERROR(hr) MOZ_WIN_EVENT_LOG_ERROR(gWinEventLogSourceName, hr)
-#define LOG_ERROR_MESSAGE(format, ...) \
- MOZ_WIN_EVENT_LOG_ERROR_MESSAGE(gWinEventLogSourceName, format, __VA_ARGS__)
+#ifdef LOG_ERRORS_FILE
+extern FILE* gLogFile;
+# define LOG_ERROR(hr) \
+ if (gLogFile) { \
+ fprintf(gLogFile, "Error in %s:%d: 0x%X\r\n", __FILE__, __LINE__, \
+ (unsigned int)hr); \
+ }
+# define LOG_ERROR_MESSAGE(format, ...) \
+ if (gLogFile) { \
+ fwprintf(gLogFile, format __VA_OPT__(, ) __VA_ARGS__); \
+ fputs("\r\n", gLogFile); \
+ }
+#else
+# define LOG_ERROR(hr)
+# define LOG_ERROR_MESSAGE(format, ...)
+#endif
#endif // __DEFAULT_BROWSER_AGENT_EVENT_LOG_H__
=====================================
toolkit/mozapps/defaultagent/SetDefaultBrowser.cpp
=====================================
@@ -444,9 +444,10 @@ nsresult SetDefaultBrowserUserChoice(
return NS_ERROR_FAILURE;
}
- nsTArray<nsString> browserDefaults = {
- u"https"_ns, u"FirefoxURL"_ns, u"http"_ns, u"FirefoxURL"_ns,
- u".html"_ns, u"FirefoxHTML"_ns, u".htm"_ns, u"FirefoxHTML"_ns};
+ nsTArray<nsString> browserDefaults = {u"https"_ns, u"MullvadBrowserURL"_ns,
+ u"http"_ns, u"MullvadBrowserURL"_ns,
+ u".html"_ns, u"MullvadBrowserHTML"_ns,
+ u".htm"_ns, u"MullvadBrowserHTML"_ns};
browserDefaults.AppendElements(aExtraFileExtensions);
=====================================
widget/windows/WinTaskbar.cpp
=====================================
@@ -249,7 +249,7 @@ bool WinTaskbar::GenerateAppUserModelID(nsAString& aAppUserModelId,
nsCString appName;
if (appInfo && NS_SUCCEEDED(appInfo->GetName(appName))) {
nsAutoString regKey;
- regKey.AssignLiteral("Software\\Mozilla\\");
+ regKey.AssignLiteral("Software\\" MOZ_APP_VENDOR "\\");
AppendASCIItoUTF16(appName, regKey);
regKey.AppendLiteral("\\TaskBarIDs");
=====================================
widget/windows/moz.build
=====================================
@@ -209,6 +209,7 @@ DEFINES["MOZ_UNICODE"] = True
DEFINES["MOZ_APP_NAME"] = '"%s"' % CONFIG["MOZ_APP_NAME"]
# Turn `firefox` into `Firefox`.
DEFINES["MOZ_TOAST_APP_NAME"] = '"%s"' % CONFIG["MOZ_APP_NAME"].title()
+DEFINES["MOZ_APP_VENDOR"] = '"%s"' % CONFIG["MOZ_APP_VENDOR"]
USE_LIBS += [
"jsoncpp",
View it on GitLab: https://gitlab.torproject.org/tpo/applications/mullvad-browser/-/commit/080…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/mullvad-browser/-/commit/080…
You're receiving this email because of your account on gitlab.torproject.org.
1
0
[Git][tpo/applications/tor-browser-build][main] 2 commits: Bug 41627: Build OpenSSL in the Python package.
by Pier Angelo Vendrame (@pierov) 11 Dec '25
by Pier Angelo Vendrame (@pierov) 11 Dec '25
11 Dec '25
Pier Angelo Vendrame pushed to branch main at The Tor Project / Applications / tor-browser-build
Commits:
b0b67b81 by Pier Angelo Vendrame at 2025-12-10T21:38:25+01:00
Bug 41627: Build OpenSSL in the Python package.
Build OpenSSL with Python, so that the ssl module can be imported.
We do not use the openssl project to avoid re-building a consistent
part of the Linux toolchain every time we bump it.
Also, bump the Python version since we are at it (we downgraded to be
able to build it with OpenSSL 1.1.x, now we don't have that requirement
anymore), and bump ninja, as the old version didn't build with our new
version of Python.
- - - - -
75825497 by Pier Angelo Vendrame at 2025-12-10T21:39:51+01:00
Bug 41662: Add python-zstandard to desktop containers.
They are needed when we build artifacts.
- - - - -
12 changed files:
- projects/common/list_toolchain_updates-common-firefox-geckoview
- projects/firefox/build
- projects/firefox/config
- projects/ninja/build
- projects/ninja/config
- − projects/ninja/python3.patch
- + projects/python-zstandard/README.md
- + projects/python-zstandard/build
- + projects/python-zstandard/config
- projects/python/README.md
- projects/python/build
- projects/python/config
Changes:
=====================================
projects/common/list_toolchain_updates-common-firefox-geckoview
=====================================
@@ -159,7 +159,7 @@ if (m/^\\s*MINIMUM_MINOR_VERSION = ([0-9]+)/) {
}
EOF
needed=3.$(cat python/mozboot/bin/bootstrap.py | perl -ne "$p")
-current="3.9" # 3.11.x on Debian bookworm, 3.9.20 on our python project
+current="3.9" # 3.11.x on Debian bookworm, 3.13.11 on our python project
check_update_needed python "$needed" "$current"
=====================================
projects/firefox/build
=====================================
@@ -34,6 +34,8 @@ export PATH="/var/tmp/dist/rust/bin:/var/tmp/dist/cbindgen:/var/tmp/dist/node/bi
tar -C /var/tmp/dist -xf [% c('input_files_by_name/clang') %]
tar -C /var/tmp/dist -xf [% c('input_files_by_name/python') %]
export PATH="/var/tmp/dist/python/bin:$PATH"
+ # For OpenSSL, see Python's README.md.
+ export LD_LIBRARY_PATH=/var/tmp/dist/python/lib:$LD_LIBRARY_PATH
[% IF ! c("var/linux-cross") -%]
tar -C /var/tmp/dist -xf $rootdir/[% c('input_files_by_name/binutils') %]
export PATH="/var/tmp/dist/binutils/bin:$PATH"
@@ -48,6 +50,9 @@ export PATH="/var/tmp/dist/rust/bin:/var/tmp/dist/cbindgen:/var/tmp/dist/node/bi
# down properly in that case. Thus, we set it here in the build script.
export PKG_CONFIG_PATH="${PKG_CONFIG_PATH}:/usr/lib/[% c("var/crosstarget") %]/pkgconfig"
[% END -%]
+ [% IF c("var/dev_artifacts") -%]
+ python3 -m pip install $rootdir/[% c('input_files_by_name/python-zstandard') %]/*.whl
+ [% END -%]
[% END -%]
[% IF c("var/macos") && c("var/dev_artifacts") %]
=====================================
projects/firefox/config
=====================================
@@ -154,6 +154,7 @@ targets:
arch_deps:
- python3
- python3-distutils
+ - python3-zstandard
- rsync
windows:
@@ -161,6 +162,7 @@ targets:
arch_deps:
- python3
- python3-distutils
+ - python3-zstandard
- wine
input_files:
@@ -228,6 +230,9 @@ input_files:
enable: '[% c("var/mullvad-browser") && c("var/has_l10n") %]'
- filename: marsigner.der
enable: '[% c("var/override_updater_url") %]'
+ - project: python-zstandard
+ enable: '[% c("var/linux") && c("var/dev_artifacts") %]'
+ name: python-zstandard
- filename: dmg-root
enable: '[% c("var/macos") && c("var/dev_artifacts") %]'
- project: hfsplus-tools
=====================================
projects/ninja/build
=====================================
@@ -12,8 +12,6 @@ mkdir -p /var/tmp/build
tar -C /var/tmp/build -xf [% project %]-[% c('version') %].tar.[% c('compress_tar') %]
cd /var/tmp/build/[% project %]-[% c('version') %]
-patch -p1 < "$rootdir/python3.patch"
-
./configure.py --bootstrap
mkdir -p $distdir
=====================================
projects/ninja/config
=====================================
@@ -1,7 +1,7 @@
# vim: filetype=yaml sw=2
-version: '[% c("abbrev") %]'
+version: '1.13.2'
git_url: https://github.com/ninja-build/ninja.git
-git_hash: a524bf3f6bacd1b4ad85d719eed2737d8562f27a #v1.11.1
+git_hash: 3441b633c2fe2c494e958780ba0f4227b1327634 # v1.13.2
filename: '[% project %]-[% c("version") %]-[% c("var/build_id") %].tar.[% c("compress_tar") %]'
container:
use_container: 1
@@ -16,4 +16,3 @@ input_files:
- name: python
project: python
enable: '[% c("var/linux") %]'
- - filename: python3.patch
=====================================
projects/ninja/python3.patch deleted
=====================================
@@ -1,95 +0,0 @@
-From 6a17e84370064eec6f22cfb1717ab80cf898d82b Mon Sep 17 00:00:00 2001
-From: Nico Weber <thakis(a)chromium.org>
-Date: Tue, 10 Jan 2023 10:46:45 -0500
-Subject: [PATCH] Use python3 in all run lines
-
-We already did this in some, this converts the rest.
-Also chmod +x on write_fake_manifests.py while here.
----
- configure.py | 4 +---
- misc/measure.py | 4 +---
- misc/ninja_syntax_test.py | 2 +-
- misc/write_fake_manifests.py | 2 +-
- src/browse.py | 4 +---
- 5 files changed, 5 insertions(+), 11 deletions(-)
- mode change 100644 => 100755 misc/write_fake_manifests.py
-
-diff --git a/configure.py b/configure.py
-index 09c5b283e0..588250aa8a 100755
---- a/configure.py
-+++ b/configure.py
-@@ -1,4 +1,4 @@
--#!/usr/bin/env python
-+#!/usr/bin/env python3
- #
- # Copyright 2001 Google Inc. All Rights Reserved.
- #
-@@ -19,8 +19,6 @@
- Projects that use ninja themselves should either write a similar script
- or use a meta-build system that supports Ninja output."""
-
--from __future__ import print_function
--
- from optparse import OptionParser
- import os
- import pipes
-diff --git a/misc/measure.py b/misc/measure.py
-index 8ce95e696b..f3825efbb0 100755
---- a/misc/measure.py
-+++ b/misc/measure.py
-@@ -1,4 +1,4 @@
--#!/usr/bin/env python
-+#!/usr/bin/env python3
-
- # Copyright 2011 Google Inc. All Rights Reserved.
- #
-@@ -17,8 +17,6 @@
- """measure the runtime of a command by repeatedly running it.
- """
-
--from __future__ import print_function
--
- import time
- import subprocess
- import sys
-diff --git a/misc/ninja_syntax_test.py b/misc/ninja_syntax_test.py
-index 90ff9c6bdb..61fb177d43 100755
---- a/misc/ninja_syntax_test.py
-+++ b/misc/ninja_syntax_test.py
-@@ -1,4 +1,4 @@
--#!/usr/bin/env python
-+#!/usr/bin/env python3
-
- # Copyright 2011 Google Inc. All Rights Reserved.
- #
-diff --git a/misc/write_fake_manifests.py b/misc/write_fake_manifests.py
-old mode 100644
-new mode 100755
-index abcb677e18..bf9cf7de92
---- a/misc/write_fake_manifests.py
-+++ b/misc/write_fake_manifests.py
-@@ -1,4 +1,4 @@
--#!/usr/bin/env python
-+#!/usr/bin/env python3
-
- """Writes large manifest files, for manifest parser performance testing.
-
-diff --git a/src/browse.py b/src/browse.py
-index 653cbe91f6..b125e805a9 100755
---- a/src/browse.py
-+++ b/src/browse.py
-@@ -1,4 +1,4 @@
--#!/usr/bin/env python
-+#!/usr/bin/env python3
- #
- # Copyright 2001 Google Inc. All Rights Reserved.
- #
-@@ -20,8 +20,6 @@
- it when needed.
- """
-
--from __future__ import print_function
--
- try:
- import http.server as httpserver
- import socketserver
=====================================
projects/python-zstandard/README.md
=====================================
@@ -0,0 +1,4 @@
+Mozilla uses the python-zstandard module for various tasks in their CI, but
+they do not vendor it in Firefox's source tree.
+
+Since we build our own Python on Linux, we also need to build this module.
=====================================
projects/python-zstandard/build
=====================================
@@ -0,0 +1,14 @@
+#!/bin/bash
+[% c("var/set_default_env") -%]
+outdir=[% dest_dir _ '/' _ c('filename') %]
+mkdir -p /var/tmp/dist $outdir
+
+tar -C /var/tmp/dist -xf $rootdir/[% c('input_files_by_name/python') %]
+export PATH=/var/tmp/dist/python/bin:$PATH
+
+python3 -m pip install *.whl
+
+tar -xf [% project %]-[% c('version') %].tar.[% c('compress_tar') %]
+cd [% project %]-[% c('version') %]
+python3 setup.py bdist_wheel
+mv dist/*.whl $outdir/
=====================================
projects/python-zstandard/config
=====================================
@@ -0,0 +1,19 @@
+version: 0.24.0
+filename: 'python-zstandard-[% c("version") %]-[% c("var/build_id") %]'
+git_url: https://github.com/indygreg/python-zstandard.git
+git_hash: 9223924b1db8f36cf1c7c2dcd55232093890d145 # 0.24.0
+container:
+ use_container: 1
+
+input_files:
+ - project: container-image
+ - project: python
+ name: python
+ # This contains only binaries for Windows.
+ - name: setuptools
+ URL: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82f…
+ sha256sum: 062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922
+ # This contains only Python.
+ - name: packaging
+ URL: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895…
+ sha256sum: 29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484
=====================================
projects/python/README.md
=====================================
@@ -17,3 +17,17 @@ image.
`browser` is a notable exception: we redefine `var/deps` for all platforms and
already add `python3` there.
+
+## OpenSSL
+
+Some Python module complain about the OpenSSL version of the container being
+too old. Therefore, we also build OpenSSL in this project, and other projects
+needing it must add `/var/tmp/dist/python/lib` to `LD_LIBRARY_PATH`.
+
+We do it here instead of using the `openssl` project because we do not want to
+rebuild a big part of the toolchain for each OpenSSL update (the module would
+be used mostly for HTTP requests, which will not go through in our builds,
+since they happen offline).
+
+When updating to a newever version of Debian for Linux containers, we might
+stop building OpenSSL and go back to using the system library.
=====================================
projects/python/build
=====================================
@@ -6,14 +6,28 @@
[% END -%]
distdir=/var/tmp/dist/[% project %]
-mkdir -p $distdir
+openssldir=/var/tmp/dist/openssl
+mkdir -p $distdir $openssldir
+
+tar -xf $rootdir/[% c('input_files_by_name/openssl') %]
+pushd openssl-3.5.4
+./Configure --prefix=$openssldir --libdir=lib
+make -j[% c("num_procs") %]
+make -j[% c("num_procs") %] install
+# Python will try to load OpenSSL during its build process.
+export LD_LIBRARY_PATH=$openssldir/lib:$LD_LIBRARY_PATH
+popd
+
tar xf [% c('input_files_by_name/python') %]
cd Python-[% c('version') %]
-./configure --prefix=$distdir --enable-optimizations
+./configure --prefix=$distdir --enable-optimizations --with-openssl=$openssldir
make -j[% c("num_procs") %]
make prefix=$distdir install
+
+cp -a $openssldir/lib/lib*.so* $distdir/lib/
+
cd /var/tmp/dist
[% c('tar', {
- tar_src => 'python',
- tar_args => '-caf ' _ dest_dir _ '/' _ c('filename'),
- }) %]
+ tar_src => 'python',
+ tar_args => '-caf ' _ dest_dir _ '/' _ c('filename'),
+ }) %]
=====================================
projects/python/config
=====================================
@@ -1,6 +1,6 @@
# vim: filetype=yaml sw=2
-version: 3.9.20
-filename: 'python-[% c("var/build_id") %].tar.[% c("compress_tar") %]'
+version: 3.13.11
+filename: 'python-[% c("version") %]-[% c("var/build_id") %].tar.[% c("compress_tar") %]'
container:
use_container: 1
var:
@@ -12,7 +12,6 @@ var:
- libffi-dev
- libncurses-dev
- libsqlite3-dev
- - libssl-dev
- zlib1g-dev
setup: |
@@ -24,7 +23,14 @@ input_files:
- project: container-image
- name: python
URL: 'https://www.python.org/ftp/python/[% c("version") %]/Python-[% c("version") %].tar.xz'
- sha256sum: 6b281279efd85294d2d6993e173983a57464c0133956fbbb5536ec9646beaf0c
+ sha256sum: 16ede7bb7cdbfa895d11b0642fa0e523f291e6487194d53cf6d3b338c3a17ea2
- name: '[% c("var/compiler") %]'
project: '[% c("var/compiler") %]'
enable: '[% c("var/linux") %]'
+ # We do not use the same we use as tor dependency because we do not want to
+ # rebuild Clang, Rust and other projects at each OpenSSL update.
+ # FWIW, these OpenSSL would be used for requests that will not go through,
+ # as our builds run offline.
+ - name: openssl
+ URL: 'https://github.com/openssl/openssl/releases/download/openssl-3.5.4/openssl-…'
+ sha256sum: 967311f84955316969bdb1d8d4b983718ef42338639c621ec4c34fddef355e99
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser-build/-/compare/…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser-build/-/compare/…
You're receiving this email because of your account on gitlab.torproject.org.
1
0
[Git][tpo/applications/tor-browser][base-browser-146.0a1-16.0-2] 3 commits: fixup! BB 41459: WebRTC fails to build under mingw (Part 2)
by Pier Angelo Vendrame (@pierov) 11 Dec '25
by Pier Angelo Vendrame (@pierov) 11 Dec '25
11 Dec '25
Pier Angelo Vendrame pushed to branch base-browser-146.0a1-16.0-2 at The Tor Project / Applications / Tor Browser
Commits:
5bfdfd0b by june wilde at 2025-12-11T12:13:24+01:00
fixup! BB 41459: WebRTC fails to build under mingw (Part 2)
- - - - -
e6e81938 by june wilde at 2025-12-11T12:13:25+01:00
fixup! BB 41459: WebRTC fails to build under mingw (Part 1)
- - - - -
f12904c2 by june wilde at 2025-12-11T12:13:25+01:00
fixup! BB 41459: WebRTC fails to build under mingw (Part 5)
- - - - -
6 changed files:
- build/moz.configure/windows.configure
- dom/media/webrtc/libwebrtc_overrides/modules/desktop_capture/desktop_capture_types.h
- third_party/libwebrtc/modules/desktop_capture/win/wgc_capture_session.cc
- third_party/libwebrtc/modules/desktop_capture/win/window_capture_utils.h
- third_party/libwebrtc/rtc_base/cpu_info.cc
- third_party/libwebrtc/rtc_base/win/create_direct3d_device.h
Changes:
=====================================
build/moz.configure/windows.configure
=====================================
@@ -624,12 +624,13 @@ with only_when(depends(c_compiler)(lambda c: c.type == "clang-cl")):
add_linker_flag("-LARGEADDRESSAWARE")
add_linker_flag("-SAFESEH")
- # avoid conficts with std::min/max
- set_define("NOMINMAX", True)
-
set_define("WIN32_LEAN_AND_MEAN", True)
+with only_when(depends(c_compiler)(lambda c: c.type == "clang-cl")):
+ # See http://support.microsoft.com/kb/143208 to use STL
+ set_define("NOMINMAX", True)
+
with only_when(target_is_windows & depends(c_compiler)(lambda c: c.type != "clang-cl")):
# strsafe.h on mingw uses macros for function deprecation that pollutes namespace
=====================================
dom/media/webrtc/libwebrtc_overrides/modules/desktop_capture/desktop_capture_types.h
=====================================
@@ -7,11 +7,11 @@
#ifndef DOM_MEDIA_WEBRTC_LIBWEBRTCOVERRIDES_MODULES_DESKTOP_CAPTURE_DESKTOP_CAPTURE_TYPES_H_
#define DOM_MEDIA_WEBRTC_LIBWEBRTCOVERRIDES_MODULES_DESKTOP_CAPTURE_DESKTOP_CAPTURE_TYPES_H_
-// pid_t
-#if !defined(XP_WIN) || defined(__MINGW32__)
+#if defined(XP_WIN) && \
+ !defined(__MINGW32__) // Moving this into the global namespace
+typedef int pid_t; // matching what used to be in
+#elif defined(XP_WIN) // video_capture_defines.h
# include <sys/types.h>
-#else
-typedef int pid_t;
#endif
#include "../../third_party/libwebrtc/modules/desktop_capture/desktop_capture_types.h"
=====================================
third_party/libwebrtc/modules/desktop_capture/win/wgc_capture_session.cc
=====================================
@@ -13,6 +13,8 @@
#include <dispatcherqueue.h>
#include <windows.graphics.capture.interop.h>
#include <windows.graphics.directx.direct3d11.interop.h>
+#include <windows.graphics.h>
+#include <wrl/client.h>
#include <wrl/event.h>
#include <algorithm>
=====================================
third_party/libwebrtc/modules/desktop_capture/win/window_capture_utils.h
=====================================
@@ -11,7 +11,7 @@
#ifndef MODULES_DESKTOP_CAPTURE_WIN_WINDOW_CAPTURE_UTILS_H_
#define MODULES_DESKTOP_CAPTURE_WIN_WINDOW_CAPTURE_UTILS_H_
-#include <shlobj_core.h>
+#include <shlobj.h>
#include <windows.h>
#include <wrl/client.h>
=====================================
third_party/libwebrtc/rtc_base/cpu_info.cc
=====================================
@@ -97,7 +97,7 @@ uint64_t xgetbv(uint32_t xcr) {
}
#endif // WEBRTC_ENABLE_AVX2
-#ifndef _MSC_VER
+#if !defined(_MSC_VER) && !defined(__MINGW32__)
// Intrinsic for "cpuid".
#if defined(__pic__) && defined(__i386__)
static inline void __cpuid(int cpu_info[4], int info_type) {
=====================================
third_party/libwebrtc/rtc_base/win/create_direct3d_device.h
=====================================
@@ -12,9 +12,8 @@
#define RTC_BASE_WIN_CREATE_DIRECT3D_DEVICE_H_
#include <windows.graphics.directx.direct3d11.h>
-#ifndef __MINGW32__
-# include <windows.graphics.directX.direct3d11.interop.h>
-#else
+#include <windows.graphics.directx.direct3d11.interop.h>
+#ifdef __MINGW32__
# include <dxgi.h>
# include <inspectable.h>
extern "C" {
@@ -23,6 +22,7 @@ HRESULT __stdcall CreateDirect3D11DeviceFromDXGIDevice(
::IDXGIDevice* dxgiDevice, ::IInspectable** graphicsDevice);
}
#endif
+
#include <winerror.h>
#include <wrl/client.h>
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/e2a7c7…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/e2a7c7…
You're receiving this email because of your account on gitlab.torproject.org.
1
0
[Git][tpo/applications/tor-browser][tor-browser-146.0a1-16.0-2] 3 commits: fixup! BB 41459: WebRTC fails to build under mingw (Part 2)
by Pier Angelo Vendrame (@pierov) 11 Dec '25
by Pier Angelo Vendrame (@pierov) 11 Dec '25
11 Dec '25
Pier Angelo Vendrame pushed to branch tor-browser-146.0a1-16.0-2 at The Tor Project / Applications / Tor Browser
Commits:
54d4d5c6 by june wilde at 2025-12-11T12:11:56+01:00
fixup! BB 41459: WebRTC fails to build under mingw (Part 2)
- - - - -
186f0f1a by june wilde at 2025-12-11T12:12:04+01:00
fixup! BB 41459: WebRTC fails to build under mingw (Part 1)
- - - - -
9596a569 by june wilde at 2025-12-11T12:12:05+01:00
fixup! BB 41459: WebRTC fails to build under mingw (Part 5)
- - - - -
6 changed files:
- build/moz.configure/windows.configure
- dom/media/webrtc/libwebrtc_overrides/modules/desktop_capture/desktop_capture_types.h
- third_party/libwebrtc/modules/desktop_capture/win/wgc_capture_session.cc
- third_party/libwebrtc/modules/desktop_capture/win/window_capture_utils.h
- third_party/libwebrtc/rtc_base/cpu_info.cc
- third_party/libwebrtc/rtc_base/win/create_direct3d_device.h
Changes:
=====================================
build/moz.configure/windows.configure
=====================================
@@ -624,12 +624,13 @@ with only_when(depends(c_compiler)(lambda c: c.type == "clang-cl")):
add_linker_flag("-LARGEADDRESSAWARE")
add_linker_flag("-SAFESEH")
- # avoid conficts with std::min/max
- set_define("NOMINMAX", True)
-
set_define("WIN32_LEAN_AND_MEAN", True)
+with only_when(depends(c_compiler)(lambda c: c.type == "clang-cl")):
+ # See http://support.microsoft.com/kb/143208 to use STL
+ set_define("NOMINMAX", True)
+
with only_when(target_is_windows & depends(c_compiler)(lambda c: c.type != "clang-cl")):
# strsafe.h on mingw uses macros for function deprecation that pollutes namespace
=====================================
dom/media/webrtc/libwebrtc_overrides/modules/desktop_capture/desktop_capture_types.h
=====================================
@@ -7,11 +7,11 @@
#ifndef DOM_MEDIA_WEBRTC_LIBWEBRTCOVERRIDES_MODULES_DESKTOP_CAPTURE_DESKTOP_CAPTURE_TYPES_H_
#define DOM_MEDIA_WEBRTC_LIBWEBRTCOVERRIDES_MODULES_DESKTOP_CAPTURE_DESKTOP_CAPTURE_TYPES_H_
-// pid_t
-#if !defined(XP_WIN) || defined(__MINGW32__)
+#if defined(XP_WIN) && \
+ !defined(__MINGW32__) // Moving this into the global namespace
+typedef int pid_t; // matching what used to be in
+#elif defined(XP_WIN) // video_capture_defines.h
# include <sys/types.h>
-#else
-typedef int pid_t;
#endif
#include "../../third_party/libwebrtc/modules/desktop_capture/desktop_capture_types.h"
=====================================
third_party/libwebrtc/modules/desktop_capture/win/wgc_capture_session.cc
=====================================
@@ -13,6 +13,8 @@
#include <dispatcherqueue.h>
#include <windows.graphics.capture.interop.h>
#include <windows.graphics.directx.direct3d11.interop.h>
+#include <windows.graphics.h>
+#include <wrl/client.h>
#include <wrl/event.h>
#include <algorithm>
=====================================
third_party/libwebrtc/modules/desktop_capture/win/window_capture_utils.h
=====================================
@@ -11,7 +11,7 @@
#ifndef MODULES_DESKTOP_CAPTURE_WIN_WINDOW_CAPTURE_UTILS_H_
#define MODULES_DESKTOP_CAPTURE_WIN_WINDOW_CAPTURE_UTILS_H_
-#include <shlobj_core.h>
+#include <shlobj.h>
#include <windows.h>
#include <wrl/client.h>
=====================================
third_party/libwebrtc/rtc_base/cpu_info.cc
=====================================
@@ -97,7 +97,7 @@ uint64_t xgetbv(uint32_t xcr) {
}
#endif // WEBRTC_ENABLE_AVX2
-#ifndef _MSC_VER
+#if !defined(_MSC_VER) && !defined(__MINGW32__)
// Intrinsic for "cpuid".
#if defined(__pic__) && defined(__i386__)
static inline void __cpuid(int cpu_info[4], int info_type) {
=====================================
third_party/libwebrtc/rtc_base/win/create_direct3d_device.h
=====================================
@@ -12,9 +12,8 @@
#define RTC_BASE_WIN_CREATE_DIRECT3D_DEVICE_H_
#include <windows.graphics.directx.direct3d11.h>
-#ifndef __MINGW32__
-# include <windows.graphics.directX.direct3d11.interop.h>
-#else
+#include <windows.graphics.directx.direct3d11.interop.h>
+#ifdef __MINGW32__
# include <dxgi.h>
# include <inspectable.h>
extern "C" {
@@ -23,6 +22,7 @@ HRESULT __stdcall CreateDirect3D11DeviceFromDXGIDevice(
::IDXGIDevice* dxgiDevice, ::IInspectable** graphicsDevice);
}
#endif
+
#include <winerror.h>
#include <wrl/client.h>
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/773d4e…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/773d4e…
You're receiving this email because of your account on gitlab.torproject.org.
1
0
[Git][tpo/applications/mullvad-browser][mullvad-browser-146.0a1-16.0-2] 4 commits: MB 479 - Fix CXXFlags for libwebrtc in 146
by Pier Angelo Vendrame (@pierov) 11 Dec '25
by Pier Angelo Vendrame (@pierov) 11 Dec '25
11 Dec '25
Pier Angelo Vendrame pushed to branch mullvad-browser-146.0a1-16.0-2 at The Tor Project / Applications / Mullvad Browser
Commits:
ec5e89ec by june wilde at 2025-12-11T12:04:25+01:00
MB 479 - Fix CXXFlags for libwebrtc in 146
This patch will no longer be necessary in Gecko 147
- - - - -
5401bb7f by june wilde at 2025-12-11T12:04:25+01:00
fixup! BB 41459: WebRTC fails to build under mingw (Part 2)
- - - - -
7a3f13c0 by june wilde at 2025-12-11T12:04:26+01:00
fixup! BB 41459: WebRTC fails to build under mingw (Part 1)
- - - - -
dcc28544 by june wilde at 2025-12-11T12:04:26+01:00
fixup! BB 41459: WebRTC fails to build under mingw (Part 5)
- - - - -
355 changed files:
- build/moz.configure/windows.configure
- dom/media/webrtc/libwebrtc_overrides/modules/desktop_capture/desktop_capture_types.h
- third_party/libwebrtc/BUILD.gn
- third_party/libwebrtc/api/adaptation/resource_adaptation_api_gn/moz.build
- third_party/libwebrtc/api/audio/aec3_config_gn/moz.build
- third_party/libwebrtc/api/audio/aec3_factory_gn/moz.build
- third_party/libwebrtc/api/audio/audio_frame_api_gn/moz.build
- third_party/libwebrtc/api/audio/audio_processing_gn/moz.build
- third_party/libwebrtc/api/audio/audio_processing_statistics_gn/moz.build
- third_party/libwebrtc/api/audio/builtin_audio_processing_builder_gn/moz.build
- third_party/libwebrtc/api/audio_codecs/L16/audio_decoder_L16_gn/moz.build
- third_party/libwebrtc/api/audio_codecs/L16/audio_encoder_L16_gn/moz.build
- third_party/libwebrtc/api/audio_codecs/audio_codecs_api_gn/moz.build
- third_party/libwebrtc/api/audio_codecs/builtin_audio_decoder_factory_gn/moz.build
- third_party/libwebrtc/api/audio_codecs/builtin_audio_encoder_factory_gn/moz.build
- third_party/libwebrtc/api/audio_codecs/g711/audio_decoder_g711_gn/moz.build
- third_party/libwebrtc/api/audio_codecs/g711/audio_encoder_g711_gn/moz.build
- third_party/libwebrtc/api/audio_codecs/g722/audio_decoder_g722_gn/moz.build
- third_party/libwebrtc/api/audio_codecs/g722/audio_encoder_g722_gn/moz.build
- third_party/libwebrtc/api/audio_codecs/opus/audio_decoder_multiopus_gn/moz.build
- third_party/libwebrtc/api/audio_codecs/opus/audio_decoder_opus_gn/moz.build
- third_party/libwebrtc/api/audio_codecs/opus/audio_encoder_multiopus_gn/moz.build
- third_party/libwebrtc/api/audio_codecs/opus/audio_encoder_opus_config_gn/moz.build
- third_party/libwebrtc/api/audio_codecs/opus/audio_encoder_opus_gn/moz.build
- third_party/libwebrtc/api/audio_codecs/opus_audio_decoder_factory_gn/moz.build
- third_party/libwebrtc/api/audio_options_api_gn/moz.build
- third_party/libwebrtc/api/crypto/options_gn/moz.build
- third_party/libwebrtc/api/environment/environment_factory_gn/moz.build
- third_party/libwebrtc/api/field_trials_registry_gn/moz.build
- third_party/libwebrtc/api/frame_transformer_factory_gn/moz.build
- third_party/libwebrtc/api/frame_transformer_interface_gn/moz.build
- third_party/libwebrtc/api/media_stream_interface_gn/moz.build
- third_party/libwebrtc/api/neteq/default_neteq_controller_factory_gn/moz.build
- third_party/libwebrtc/api/neteq/default_neteq_factory_gn/moz.build
- third_party/libwebrtc/api/neteq/neteq_api_gn/moz.build
- third_party/libwebrtc/api/neteq/tick_timer_gn/moz.build
- third_party/libwebrtc/api/priority_gn/moz.build
- third_party/libwebrtc/api/rtc_error_gn/moz.build
- third_party/libwebrtc/api/rtc_event_log/rtc_event_log_gn/moz.build
- third_party/libwebrtc/api/rtp_headers_gn/moz.build
- third_party/libwebrtc/api/rtp_packet_info_gn/moz.build
- third_party/libwebrtc/api/rtp_parameters_gn/moz.build
- third_party/libwebrtc/api/rtp_sender_setparameters_callback_gn/moz.build
- third_party/libwebrtc/api/task_queue/pending_task_safety_flag_gn/moz.build
- third_party/libwebrtc/api/task_queue/task_queue_gn/moz.build
- third_party/libwebrtc/api/transport/bitrate_settings_gn/moz.build
- third_party/libwebrtc/api/transport/field_trial_based_config_gn/moz.build
- third_party/libwebrtc/api/transport/goog_cc_gn/moz.build
- third_party/libwebrtc/api/transport/network_control_gn/moz.build
- third_party/libwebrtc/api/transport/rtp/dependency_descriptor_gn/moz.build
- third_party/libwebrtc/api/transport_api_gn/moz.build
- third_party/libwebrtc/api/units/data_rate_gn/moz.build
- third_party/libwebrtc/api/units/data_size_gn/moz.build
- third_party/libwebrtc/api/units/frequency_gn/moz.build
- third_party/libwebrtc/api/units/time_delta_gn/moz.build
- third_party/libwebrtc/api/units/timestamp_gn/moz.build
- third_party/libwebrtc/api/video/builtin_video_bitrate_allocator_factory_gn/moz.build
- third_party/libwebrtc/api/video/encoded_frame_gn/moz.build
- third_party/libwebrtc/api/video/encoded_image_gn/moz.build
- third_party/libwebrtc/api/video/frame_buffer_gn/moz.build
- third_party/libwebrtc/api/video/video_adaptation_gn/moz.build
- third_party/libwebrtc/api/video/video_bitrate_allocation_gn/moz.build
- third_party/libwebrtc/api/video/video_bitrate_allocator_gn/moz.build
- third_party/libwebrtc/api/video/video_frame_gn/moz.build
- third_party/libwebrtc/api/video/video_frame_i010_gn/moz.build
- third_party/libwebrtc/api/video/video_frame_metadata_gn/moz.build
- third_party/libwebrtc/api/video/video_rtp_headers_gn/moz.build
- third_party/libwebrtc/api/video_codecs/builtin_video_decoder_factory_gn/moz.build
- third_party/libwebrtc/api/video_codecs/rtc_software_fallback_wrappers_gn/moz.build
- third_party/libwebrtc/api/video_codecs/scalability_mode_gn/moz.build
- third_party/libwebrtc/api/video_codecs/video_codecs_api_gn/moz.build
- third_party/libwebrtc/api/video_codecs/vp8_temporal_layers_factory_gn/moz.build
- third_party/libwebrtc/audio/audio_gn/moz.build
- third_party/libwebrtc/audio/utility/audio_frame_operations_gn/moz.build
- third_party/libwebrtc/call/adaptation/resource_adaptation_gn/moz.build
- third_party/libwebrtc/call/bitrate_allocator_gn/moz.build
- third_party/libwebrtc/call/bitrate_configurator_gn/moz.build
- third_party/libwebrtc/call/call_gn/moz.build
- third_party/libwebrtc/call/call_interfaces_gn/moz.build
- third_party/libwebrtc/call/payload_type_picker_gn/moz.build
- third_party/libwebrtc/call/rtp_interfaces_gn/moz.build
- third_party/libwebrtc/call/rtp_receiver_gn/moz.build
- third_party/libwebrtc/call/rtp_sender_gn/moz.build
- third_party/libwebrtc/call/version_gn/moz.build
- third_party/libwebrtc/call/video_receive_stream_api_gn/moz.build
- third_party/libwebrtc/call/video_send_stream_api_gn/moz.build
- third_party/libwebrtc/common_audio/common_audio_avx2_gn/moz.build
- third_party/libwebrtc/common_audio/common_audio_cc_gn/moz.build
- third_party/libwebrtc/common_audio/common_audio_gn/moz.build
- third_party/libwebrtc/common_audio/common_audio_neon_gn/moz.build
- third_party/libwebrtc/common_audio/common_audio_sse2_gn/moz.build
- third_party/libwebrtc/common_audio/fir_filter_factory_gn/moz.build
- third_party/libwebrtc/common_audio/third_party/ooura/fft_size_128_gn/moz.build
- third_party/libwebrtc/common_audio/third_party/ooura/fft_size_256_gn/moz.build
- third_party/libwebrtc/common_video/common_video_gn/moz.build
- third_party/libwebrtc/common_video/corruption_detection_converters_gn/moz.build
- third_party/libwebrtc/common_video/generic_frame_descriptor/generic_frame_descriptor_gn/moz.build
- third_party/libwebrtc/logging/rtc_event_audio_gn/moz.build
- third_party/libwebrtc/logging/rtc_event_bwe_gn/moz.build
- third_party/libwebrtc/logging/rtc_event_field_gn/moz.build
- third_party/libwebrtc/logging/rtc_event_number_encodings_gn/moz.build
- third_party/libwebrtc/logging/rtc_event_pacing_gn/moz.build
- third_party/libwebrtc/logging/rtc_event_rtp_rtcp_gn/moz.build
- third_party/libwebrtc/logging/rtc_event_video_gn/moz.build
- third_party/libwebrtc/logging/rtc_stream_config_gn/moz.build
- third_party/libwebrtc/media/adapted_video_track_source_gn/moz.build
- third_party/libwebrtc/media/codec_gn/moz.build
- third_party/libwebrtc/media/media_constants_gn/moz.build
- third_party/libwebrtc/media/rid_description_gn/moz.build
- third_party/libwebrtc/media/rtc_audio_video_gn/moz.build
- third_party/libwebrtc/media/rtc_internal_video_codecs_gn/moz.build
- third_party/libwebrtc/media/rtc_sdp_video_format_utils_gn/moz.build
- third_party/libwebrtc/media/rtc_simulcast_encoder_adapter_gn/moz.build
- third_party/libwebrtc/media/video_adapter_gn/moz.build
- third_party/libwebrtc/media/video_broadcaster_gn/moz.build
- third_party/libwebrtc/media/video_common_gn/moz.build
- third_party/libwebrtc/media/video_source_base_gn/moz.build
- third_party/libwebrtc/modules/async_audio_processing/async_audio_processing_gn/moz.build
- third_party/libwebrtc/modules/audio_coding/audio_coding_gn/moz.build
- third_party/libwebrtc/modules/audio_coding/audio_coding_opus_common_gn/moz.build
- third_party/libwebrtc/modules/audio_coding/audio_encoder_cng_gn/moz.build
- third_party/libwebrtc/modules/audio_coding/audio_network_adaptor_config_gn/moz.build
- third_party/libwebrtc/modules/audio_coding/audio_network_adaptor_gn/moz.build
- third_party/libwebrtc/modules/audio_coding/g711_gn/moz.build
- third_party/libwebrtc/modules/audio_coding/g722_gn/moz.build
- third_party/libwebrtc/modules/audio_coding/legacy_encoded_audio_frame_gn/moz.build
- third_party/libwebrtc/modules/audio_coding/neteq_gn/moz.build
- third_party/libwebrtc/modules/audio_coding/pcm16b_gn/moz.build
- third_party/libwebrtc/modules/audio_coding/red_gn/moz.build
- third_party/libwebrtc/modules/audio_coding/webrtc_cng_gn/moz.build
- third_party/libwebrtc/modules/audio_coding/webrtc_multiopus_gn/moz.build
- third_party/libwebrtc/modules/audio_coding/webrtc_opus_gn/moz.build
- third_party/libwebrtc/modules/audio_coding/webrtc_opus_wrapper_gn/moz.build
- third_party/libwebrtc/modules/audio_mixer/audio_frame_manipulator_gn/moz.build
- third_party/libwebrtc/modules/audio_mixer/audio_mixer_impl_gn/moz.build
- third_party/libwebrtc/modules/audio_processing/aec3/aec3_avx2_gn/moz.build
- third_party/libwebrtc/modules/audio_processing/aec3/aec3_gn/moz.build
- third_party/libwebrtc/modules/audio_processing/aec_dump/null_aec_dump_factory_gn/moz.build
- third_party/libwebrtc/modules/audio_processing/aec_dump_interface_gn/moz.build
- third_party/libwebrtc/modules/audio_processing/aecm/aecm_core_gn/moz.build
- third_party/libwebrtc/modules/audio_processing/agc/agc_gn/moz.build
- third_party/libwebrtc/modules/audio_processing/agc/legacy_agc_gn/moz.build
- third_party/libwebrtc/modules/audio_processing/agc/level_estimation_gn/moz.build
- third_party/libwebrtc/modules/audio_processing/agc2/adaptive_digital_gain_controller_gn/moz.build
- third_party/libwebrtc/modules/audio_processing/agc2/biquad_filter_gn/moz.build
- third_party/libwebrtc/modules/audio_processing/agc2/clipping_predictor_gn/moz.build
- third_party/libwebrtc/modules/audio_processing/agc2/cpu_features_gn/moz.build
- third_party/libwebrtc/modules/audio_processing/agc2/fixed_digital_gn/moz.build
- third_party/libwebrtc/modules/audio_processing/agc2/gain_applier_gn/moz.build
- third_party/libwebrtc/modules/audio_processing/agc2/input_volume_controller_gn/moz.build
- third_party/libwebrtc/modules/audio_processing/agc2/input_volume_stats_reporter_gn/moz.build
- third_party/libwebrtc/modules/audio_processing/agc2/noise_level_estimator_gn/moz.build
- third_party/libwebrtc/modules/audio_processing/agc2/rnn_vad/rnn_vad_auto_correlation_gn/moz.build
- third_party/libwebrtc/modules/audio_processing/agc2/rnn_vad/rnn_vad_gn/moz.build
- third_party/libwebrtc/modules/audio_processing/agc2/rnn_vad/rnn_vad_layers_gn/moz.build
- third_party/libwebrtc/modules/audio_processing/agc2/rnn_vad/rnn_vad_lp_residual_gn/moz.build
- third_party/libwebrtc/modules/audio_processing/agc2/rnn_vad/rnn_vad_pitch_gn/moz.build
- third_party/libwebrtc/modules/audio_processing/agc2/rnn_vad/rnn_vad_spectral_features_gn/moz.build
- third_party/libwebrtc/modules/audio_processing/agc2/rnn_vad/vector_math_avx2_gn/moz.build
- third_party/libwebrtc/modules/audio_processing/agc2/saturation_protector_gn/moz.build
- third_party/libwebrtc/modules/audio_processing/agc2/speech_level_estimator_gn/moz.build
- third_party/libwebrtc/modules/audio_processing/agc2/vad_wrapper_gn/moz.build
- third_party/libwebrtc/modules/audio_processing/apm_logging_gn/moz.build
- third_party/libwebrtc/modules/audio_processing/audio_buffer_gn/moz.build
- third_party/libwebrtc/modules/audio_processing/audio_frame_proxies_gn/moz.build
- third_party/libwebrtc/modules/audio_processing/audio_processing_gn/moz.build
- third_party/libwebrtc/modules/audio_processing/capture_levels_adjuster/capture_levels_adjuster_gn/moz.build
- third_party/libwebrtc/modules/audio_processing/gain_controller2_gn/moz.build
- third_party/libwebrtc/modules/audio_processing/high_pass_filter_gn/moz.build
- third_party/libwebrtc/modules/audio_processing/ns/ns_gn/moz.build
- third_party/libwebrtc/modules/audio_processing/post_filter_gn/moz.build
- third_party/libwebrtc/modules/audio_processing/rms_level_gn/moz.build
- third_party/libwebrtc/modules/audio_processing/utility/cascaded_biquad_filter_gn/moz.build
- third_party/libwebrtc/modules/audio_processing/utility/legacy_delay_estimator_gn/moz.build
- third_party/libwebrtc/modules/audio_processing/utility/pffft_wrapper_gn/moz.build
- third_party/libwebrtc/modules/audio_processing/vad/vad_gn/moz.build
- third_party/libwebrtc/modules/congestion_controller/congestion_controller_gn/moz.build
- third_party/libwebrtc/modules/congestion_controller/goog_cc/alr_detector_gn/moz.build
- third_party/libwebrtc/modules/congestion_controller/goog_cc/delay_based_bwe_gn/moz.build
- third_party/libwebrtc/modules/congestion_controller/goog_cc/estimators_gn/moz.build
- third_party/libwebrtc/modules/congestion_controller/goog_cc/goog_cc_gn/moz.build
- third_party/libwebrtc/modules/congestion_controller/goog_cc/link_capacity_estimator_gn/moz.build
- third_party/libwebrtc/modules/congestion_controller/goog_cc/loss_based_bwe_v2_gn/moz.build
- third_party/libwebrtc/modules/congestion_controller/goog_cc/probe_controller_gn/moz.build
- third_party/libwebrtc/modules/congestion_controller/goog_cc/pushback_controller_gn/moz.build
- third_party/libwebrtc/modules/congestion_controller/goog_cc/send_side_bwe_gn/moz.build
- third_party/libwebrtc/modules/congestion_controller/rtp/control_handler_gn/moz.build
- third_party/libwebrtc/modules/congestion_controller/rtp/transport_feedback_gn/moz.build
- third_party/libwebrtc/modules/desktop_capture/desktop_capture_differ_sse2_gn/moz.build
- third_party/libwebrtc/modules/desktop_capture/desktop_capture_gn/moz.build
- third_party/libwebrtc/modules/desktop_capture/primitives_gn/moz.build
- third_party/libwebrtc/modules/desktop_capture/win/wgc_capture_session.cc
- third_party/libwebrtc/modules/desktop_capture/win/window_capture_utils.h
- third_party/libwebrtc/modules/pacing/interval_budget_gn/moz.build
- third_party/libwebrtc/modules/pacing/pacing_gn/moz.build
- third_party/libwebrtc/modules/remote_bitrate_estimator/congestion_control_feedback_generator_gn/moz.build
- third_party/libwebrtc/modules/remote_bitrate_estimator/remote_bitrate_estimator_gn/moz.build
- third_party/libwebrtc/modules/remote_bitrate_estimator/transport_sequence_number_feedback_generator_gn/moz.build
- third_party/libwebrtc/modules/rtp_rtcp/leb128_gn/moz.build
- third_party/libwebrtc/modules/rtp_rtcp/ntp_time_util_gn/moz.build
- third_party/libwebrtc/modules/rtp_rtcp/rtp_rtcp_format_gn/moz.build
- third_party/libwebrtc/modules/rtp_rtcp/rtp_rtcp_gn/moz.build
- third_party/libwebrtc/modules/rtp_rtcp/rtp_video_header_gn/moz.build
- third_party/libwebrtc/modules/video_capture/video_capture_internal_impl_gn/moz.build
- third_party/libwebrtc/modules/video_capture/video_capture_module_gn/moz.build
- third_party/libwebrtc/modules/video_coding/chain_diff_calculator_gn/moz.build
- third_party/libwebrtc/modules/video_coding/codecs/av1/av1_svc_config_gn/moz.build
- third_party/libwebrtc/modules/video_coding/codecs/av1/dav1d_decoder_gn/moz.build
- third_party/libwebrtc/modules/video_coding/codecs/av1/libaom_av1_encoder_gn/moz.build
- third_party/libwebrtc/modules/video_coding/encoded_frame_gn/moz.build
- third_party/libwebrtc/modules/video_coding/frame_dependencies_calculator_gn/moz.build
- third_party/libwebrtc/modules/video_coding/frame_helpers_gn/moz.build
- third_party/libwebrtc/modules/video_coding/h264_sprop_parameter_sets_gn/moz.build
- third_party/libwebrtc/modules/video_coding/h26x_packet_buffer_gn/moz.build
- third_party/libwebrtc/modules/video_coding/nack_requester_gn/moz.build
- third_party/libwebrtc/modules/video_coding/packet_buffer_gn/moz.build
- third_party/libwebrtc/modules/video_coding/svc/scalability_mode_util_gn/moz.build
- third_party/libwebrtc/modules/video_coding/svc/scalability_structures_gn/moz.build
- third_party/libwebrtc/modules/video_coding/svc/scalable_video_controller_gn/moz.build
- third_party/libwebrtc/modules/video_coding/svc/simulcast_to_svc_converter_gn/moz.build
- third_party/libwebrtc/modules/video_coding/svc/svc_rate_allocator_gn/moz.build
- third_party/libwebrtc/modules/video_coding/timing/decode_time_percentile_filter_gn/moz.build
- third_party/libwebrtc/modules/video_coding/timing/frame_delay_variation_kalman_filter_gn/moz.build
- third_party/libwebrtc/modules/video_coding/timing/inter_frame_delay_variation_calculator_gn/moz.build
- third_party/libwebrtc/modules/video_coding/timing/jitter_estimator_gn/moz.build
- third_party/libwebrtc/modules/video_coding/timing/rtt_filter_gn/moz.build
- third_party/libwebrtc/modules/video_coding/timing/timestamp_extrapolator_gn/moz.build
- third_party/libwebrtc/modules/video_coding/timing/timing_module_gn/moz.build
- third_party/libwebrtc/modules/video_coding/video_codec_interface_gn/moz.build
- third_party/libwebrtc/modules/video_coding/video_coding_gn/moz.build
- third_party/libwebrtc/modules/video_coding/video_coding_utility_gn/moz.build
- third_party/libwebrtc/modules/video_coding/webrtc_h264_gn/moz.build
- third_party/libwebrtc/modules/video_coding/webrtc_libvpx_interface_gn/moz.build
- third_party/libwebrtc/modules/video_coding/webrtc_vp8_gn/moz.build
- third_party/libwebrtc/modules/video_coding/webrtc_vp8_scalability_gn/moz.build
- third_party/libwebrtc/modules/video_coding/webrtc_vp8_temporal_layers_gn/moz.build
- third_party/libwebrtc/modules/video_coding/webrtc_vp9_gn/moz.build
- third_party/libwebrtc/modules/video_coding/webrtc_vp9_helpers_gn/moz.build
- third_party/libwebrtc/moz-patch-stack/s0129.patch
- third_party/libwebrtc/net/dcsctp/packet/chunk_gn/moz.build
- third_party/libwebrtc/net/dcsctp/packet/chunk_validators_gn/moz.build
- third_party/libwebrtc/net/dcsctp/packet/crc32c_gn/moz.build
- third_party/libwebrtc/net/dcsctp/packet/error_cause_gn/moz.build
- third_party/libwebrtc/net/dcsctp/packet/parameter_gn/moz.build
- third_party/libwebrtc/net/dcsctp/packet/sctp_packet_gn/moz.build
- third_party/libwebrtc/net/dcsctp/packet/tlv_trait_gn/moz.build
- third_party/libwebrtc/net/dcsctp/public/factory_gn/moz.build
- third_party/libwebrtc/net/dcsctp/public/socket_gn/moz.build
- third_party/libwebrtc/net/dcsctp/rx/data_tracker_gn/moz.build
- third_party/libwebrtc/net/dcsctp/rx/interleaved_reassembly_streams_gn/moz.build
- third_party/libwebrtc/net/dcsctp/rx/reassembly_queue_gn/moz.build
- third_party/libwebrtc/net/dcsctp/rx/traditional_reassembly_streams_gn/moz.build
- third_party/libwebrtc/net/dcsctp/socket/dcsctp_socket_gn/moz.build
- third_party/libwebrtc/net/dcsctp/socket/heartbeat_handler_gn/moz.build
- third_party/libwebrtc/net/dcsctp/socket/packet_sender_gn/moz.build
- third_party/libwebrtc/net/dcsctp/socket/stream_reset_handler_gn/moz.build
- third_party/libwebrtc/net/dcsctp/socket/transmission_control_block_gn/moz.build
- third_party/libwebrtc/net/dcsctp/timer/task_queue_timeout_gn/moz.build
- third_party/libwebrtc/net/dcsctp/timer/timer_gn/moz.build
- third_party/libwebrtc/net/dcsctp/tx/outstanding_data_gn/moz.build
- third_party/libwebrtc/net/dcsctp/tx/retransmission_error_counter_gn/moz.build
- third_party/libwebrtc/net/dcsctp/tx/retransmission_queue_gn/moz.build
- third_party/libwebrtc/net/dcsctp/tx/retransmission_timeout_gn/moz.build
- third_party/libwebrtc/net/dcsctp/tx/rr_send_queue_gn/moz.build
- third_party/libwebrtc/net/dcsctp/tx/stream_scheduler_gn/moz.build
- third_party/libwebrtc/rtc_base/async_dns_resolver_gn/moz.build
- third_party/libwebrtc/rtc_base/base64_gn/moz.build
- third_party/libwebrtc/rtc_base/bit_buffer_gn/moz.build
- third_party/libwebrtc/rtc_base/bitrate_tracker_gn/moz.build
- third_party/libwebrtc/rtc_base/bitstream_reader_gn/moz.build
- third_party/libwebrtc/rtc_base/byte_buffer_gn/moz.build
- third_party/libwebrtc/rtc_base/checks_gn/moz.build
- third_party/libwebrtc/rtc_base/containers/flat_containers_internal_gn/moz.build
- third_party/libwebrtc/rtc_base/copy_on_write_buffer_gn/moz.build
- third_party/libwebrtc/rtc_base/cpu_info.cc
- third_party/libwebrtc/rtc_base/cpu_info_gn/moz.build
- third_party/libwebrtc/rtc_base/criticalsection_gn/moz.build
- third_party/libwebrtc/rtc_base/denormal_disabler_gn/moz.build
- third_party/libwebrtc/rtc_base/event_tracer_gn/moz.build
- third_party/libwebrtc/rtc_base/experiments/alr_experiment_gn/moz.build
- third_party/libwebrtc/rtc_base/experiments/balanced_degradation_settings_gn/moz.build
- third_party/libwebrtc/rtc_base/experiments/encoder_info_settings_gn/moz.build
- third_party/libwebrtc/rtc_base/experiments/field_trial_parser_gn/moz.build
- third_party/libwebrtc/rtc_base/experiments/keyframe_interval_settings_experiment_gn/moz.build
- third_party/libwebrtc/rtc_base/experiments/min_video_bitrate_experiment_gn/moz.build
- third_party/libwebrtc/rtc_base/experiments/normalize_simulcast_size_experiment_gn/moz.build
- third_party/libwebrtc/rtc_base/experiments/quality_scaler_settings_gn/moz.build
- third_party/libwebrtc/rtc_base/experiments/quality_scaling_experiment_gn/moz.build
- third_party/libwebrtc/rtc_base/experiments/rate_control_settings_gn/moz.build
- third_party/libwebrtc/rtc_base/frequency_tracker_gn/moz.build
- third_party/libwebrtc/rtc_base/histogram_percentile_counter_gn/moz.build
- third_party/libwebrtc/rtc_base/ip_address_gn/moz.build
- third_party/libwebrtc/rtc_base/logging_gn/moz.build
- third_party/libwebrtc/rtc_base/memory/aligned_malloc_gn/moz.build
- third_party/libwebrtc/rtc_base/net_helpers_gn/moz.build
- third_party/libwebrtc/rtc_base/network/sent_packet_gn/moz.build
- third_party/libwebrtc/rtc_base/network_constants_gn/moz.build
- third_party/libwebrtc/rtc_base/network_route_gn/moz.build
- third_party/libwebrtc/rtc_base/null_socket_server_gn/moz.build
- third_party/libwebrtc/rtc_base/platform_thread_gn/moz.build
- third_party/libwebrtc/rtc_base/platform_thread_types_gn/moz.build
- third_party/libwebrtc/rtc_base/race_checker_gn/moz.build
- third_party/libwebrtc/rtc_base/random_gn/moz.build
- third_party/libwebrtc/rtc_base/rate_limiter_gn/moz.build
- third_party/libwebrtc/rtc_base/rate_statistics_gn/moz.build
- third_party/libwebrtc/rtc_base/rate_tracker_gn/moz.build
- third_party/libwebrtc/rtc_base/rtc_event_gn/moz.build
- third_party/libwebrtc/rtc_base/rtc_numerics_gn/moz.build
- third_party/libwebrtc/rtc_base/rtp_to_ntp_estimator_gn/moz.build
- third_party/libwebrtc/rtc_base/sample_counter_gn/moz.build
- third_party/libwebrtc/rtc_base/socket_address_gn/moz.build
- third_party/libwebrtc/rtc_base/socket_gn/moz.build
- third_party/libwebrtc/rtc_base/stringutils_gn/moz.build
- third_party/libwebrtc/rtc_base/synchronization/sequence_checker_internal_gn/moz.build
- third_party/libwebrtc/rtc_base/synchronization/yield_gn/moz.build
- third_party/libwebrtc/rtc_base/synchronization/yield_policy_gn/moz.build
- third_party/libwebrtc/rtc_base/system/file_wrapper_gn/moz.build
- third_party/libwebrtc/rtc_base/task_utils/repeating_task_gn/moz.build
- third_party/libwebrtc/rtc_base/third_party/sigslot/sigslot_gn/moz.build
- third_party/libwebrtc/rtc_base/threading_gn/moz.build
- third_party/libwebrtc/rtc_base/timeutils_gn/moz.build
- third_party/libwebrtc/rtc_base/weak_ptr_gn/moz.build
- third_party/libwebrtc/rtc_base/win/create_direct3d_device.h
- third_party/libwebrtc/rtc_base/win/create_direct3d_device_gn/moz.build
- third_party/libwebrtc/rtc_base/win/get_activation_factory_gn/moz.build
- third_party/libwebrtc/rtc_base/win/hstring_gn/moz.build
- third_party/libwebrtc/rtc_base/win/windows_version_gn/moz.build
- third_party/libwebrtc/rtc_base/win32_gn/moz.build
- third_party/libwebrtc/rtc_base/zero_memory_gn/moz.build
- third_party/libwebrtc/system_wrappers/field_trial_gn/moz.build
- third_party/libwebrtc/system_wrappers/metrics_gn/moz.build
- third_party/libwebrtc/system_wrappers/system_wrappers_gn/moz.build
- third_party/libwebrtc/video/adaptation/video_adaptation_gn/moz.build
- third_party/libwebrtc/video/config/encoder_config_gn/moz.build
- third_party/libwebrtc/video/config/streams_config_gn/moz.build
- third_party/libwebrtc/video/corruption_detection/corruption_classifier_gn/moz.build
- third_party/libwebrtc/video/corruption_detection/frame_instrumentation_evaluation_gn/moz.build
- third_party/libwebrtc/video/corruption_detection/frame_instrumentation_generator_gn/moz.build
- third_party/libwebrtc/video/corruption_detection/generic_mapping_functions_gn/moz.build
- third_party/libwebrtc/video/corruption_detection/halton_frame_sampler_gn/moz.build
- third_party/libwebrtc/video/corruption_detection/halton_sequence_gn/moz.build
- third_party/libwebrtc/video/corruption_detection/video_frame_sampler_gn/moz.build
- third_party/libwebrtc/video/decode_synchronizer_gn/moz.build
- third_party/libwebrtc/video/frame_cadence_adapter_gn/moz.build
- third_party/libwebrtc/video/frame_decode_timing_gn/moz.build
- third_party/libwebrtc/video/frame_dumping_decoder_gn/moz.build
- third_party/libwebrtc/video/frame_dumping_encoder_gn/moz.build
- third_party/libwebrtc/video/render/incoming_video_stream_gn/moz.build
- third_party/libwebrtc/video/render/video_render_frames_gn/moz.build
- third_party/libwebrtc/video/task_queue_frame_decode_scheduler_gn/moz.build
- third_party/libwebrtc/video/unique_timestamp_counter_gn/moz.build
- third_party/libwebrtc/video/video_gn/moz.build
- third_party/libwebrtc/video/video_receive_stream_timeout_tracker_gn/moz.build
- third_party/libwebrtc/video/video_stream_buffer_controller_gn/moz.build
- third_party/libwebrtc/video/video_stream_encoder_impl_gn/moz.build
The diff was not included because it is too large.
View it on GitLab: https://gitlab.torproject.org/tpo/applications/mullvad-browser/-/compare/f2…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/mullvad-browser/-/compare/f2…
You're receiving this email because of your account on gitlab.torproject.org.
1
0