This is an automated email from the ASF dual-hosted git repository.

d4rkstar pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/openserverless.git


The following commit(s) were added to refs/heads/main by this push:
     new bd5fc5d  0.9.0 rebranded (#231)
bd5fc5d is described below

commit bd5fc5d788ec20f81ec76349fb2f6f6f779163ef
Author: Michele Sciabarra <[email protected]>
AuthorDate: Tue Sep 1 21:18:18 2026 +0200

    0.9.0 rebranded (#231)
    
    * sync subrepo
    
    * fixed test and release tag
    
    * fixes for source build
    
    * no relase in git
    
    * add default email and corret target
    
    * transform to create a versioned dir
    
    * ignoring vscode
    
    * licenserc
    
    * generate release from paht
    
    * sync fixes tests
    
    * v0.9.0-RC2
    
    * added incubating
    
    * Potential fix for pull request finding
    
    Co-authored-by: Copilot Autofix powered by AI 
<[email protected]>
    
    * change global user.name and email only if unset
    
    * sync (#217)
    
    Co-authored-by: Trustable User <[email protected]>
    
    * rebranding (#228)
    
    * rebrander
    
    * rebrander with rename files
    
    * rebrander
    
    * do-it-for-real
    
    * taskfile
    
    * bug fixes
    
    * bug fixes
    
    ---------
    
    Co-authored-by: Trustable User <[email protected]>
    
    * align cli (#229)
    
    Co-authored-by: Build OpenServerless <[email protected]>
    
    * syncronization (#230)
    
    Co-authored-by: Trustable User <[email protected]>
    
    * aligning latest fixes
    
    * switched branch 0.1.0  -> 0.9.0
    
    * roadmap
    
    * synchronize the rebranded tree
    
    ---------
    
    Co-authored-by: Trustable User <[email protected]>
    Co-authored-by: Bruno Salzano <[email protected]>
    Co-authored-by: Copilot Autofix powered by AI 
<[email protected]>
    Co-authored-by: Michele Sciabarra <[email protected]>
---
 .gitignore               |   2 +
 .gitmodules              |   6 +-
 .licenserc.yaml          |   4 +
 README.md                |  16 +-
 ROADMAP.md               |   2 +-
 Taskfile.yml             |  62 ++++++-
 admin-api                |   2 +-
 build                    |   2 +-
 build-and-test-ubuntu.sh |   6 +-
 cli                      |   2 +-
 olaris                   |   2 +-
 olaris-op                |   2 +-
 rebrander.md             |  69 ++++++++
 rebrander.py             | 451 +++++++++++++++++++++++++++++++++++++++++++++++
 runtimes                 |   2 +-
 testing                  |   2 +-
 16 files changed, 607 insertions(+), 25 deletions(-)

diff --git a/.gitignore b/.gitignore
index 6f478d8..0f23818 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,3 +21,5 @@
 **/.DS_Store
 network-cache/
 ops
+RELEASE
+rebrander.log
diff --git a/.gitmodules b/.gitmodules
index 2387251..d6d0bee 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -17,7 +17,7 @@
 [submodule "admin-api"]
        path = admin-api
        url = https://github.com/apache/openserverless-admin-api
-    branch = 0.9.0
+        branch = 0.9.0
 
 [submodule "build"]
        path = build
@@ -37,12 +37,12 @@
 [submodule "openserverless-devcontainer"]
        path = devcontainer
        url = https://github.com/apache/openserverless-devcontainer
-    branch = 0.9.0
+        branch = 0.9.0
 
 [submodule "task"]
        path = olaris
        url = https://github.com/apache/openserverless-task
-       branch = 0.1.0
+       branch = 0.9.0
 
 [submodule "runtimes"]
        path = runtimes
diff --git a/.licenserc.yaml b/.licenserc.yaml
index 2c3f612..9a05917 100644
--- a/.licenserc.yaml
+++ b/.licenserc.yaml
@@ -21,6 +21,7 @@ header:
     copyright-owner: Apache Software Foundation
 
   paths-ignore:
+    - 'cli'
     - 'admin-api'
     - 'devcontainer'
     - 'olaris'
@@ -33,7 +34,10 @@ header:
     - '**/*.svg'
     - '**/*.png'
     - '**/*.md'
+    - '.vscode/**'
     - 'LICENSE'
     - 'DISCLAIMER'
     - 'NOTICE'
     - 'CHANGES'
+    - 'RELEASE'
+    - 'rebrander.log'
diff --git a/README.md b/README.md
index 2dbecc1..7761dca 100644
--- a/README.md
+++ b/README.md
@@ -11,16 +11,23 @@ Welcome to  [Apache 
OpenServerless](https://openserverless.apache.org), an incub
 # Build and test from sources
 
 > [!WARNING]
-> Building from the latest sources in git is **not recommended for production 
use**. The `main` branch may contain unstable, untested, or incomplete changes. 
For production deployments, use an official release tarball instead. There are 
no Apache Software Foundation approved releases yet.
+> Building from the latest sources in git is **not recommended for production 
use**. The `main` branch may contain unstable, untested, or incomplete changes. 
For production deployments, use an official release tarball instead.
 
-Download a releases tarball or clone all the latest sources with:
+Download a releases tarball from the Apache distribution, untar and cd to it:
 
 ```
-git clone https://github.com/apache/openserverless --recurse-submodules
+tar xzvf openserverless-<version>.tar.gz
+cd openserverless-<version>
+```
+
+or clone the latest sources from the `main` branch or a release branch:
+
+```
+git clone --branch <branch> https://github.com/apache/openserverless 
--recurse-submodules
 cd openserverless
 ```
 
-You can then build and test
+You can then build and test as follows.
 
 # Linux
 
@@ -42,7 +49,6 @@ You have to install [lima](https://limma-vm.io) (example: 
brew install lima). Ex
 
 `./build-and-test-mac.sh`
 
-
 # Development setup
 
 ## Prerequisites
diff --git a/ROADMAP.md b/ROADMAP.md
index 05d016f..122f89f 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -12,7 +12,7 @@ The order of the items does not pretend to to establish the 
project priorities.
     * Improvement: Migrate to newer version of MINIO server
     * Improvement: Migrate to newer version os Postgres SQL
     * Improvement: Update to latest kopf version and latest Python version
-    * Improvement: Deploy default nuvolaris namespace as a normal wsku 
resources
+    * Improvement: Deploy default openserverless namespace as a normal wsku 
resources
     * Improvement: Handle CouchDb init task as Operator managed task
     * Improvement: Use base images with smaller size for fast loading
     * New: Integrate Argo Events and implements system action to support 
deployment of argo events resources
diff --git a/Taskfile.yml b/Taskfile.yml
index 563ec25..4f4fcdb 100644
--- a/Taskfile.yml
+++ b/Taskfile.yml
@@ -18,11 +18,22 @@
 version: 3
 
 vars:
+  RELEASE_FROM_DIR: '{{ if regexMatch "v[0-9][^/]+$" (base .TASKFILE_DIR) }}{{ 
regexFind "v[0-9][^/]+$" (base .TASKFILE_DIR) }}{{ else }}snapshot{{ end }}'
+  RELEASE:
+    sh: |
+        if test -e .git
+        then git -C olaris-op rev-parse --short HEAD
+        else
+          if test -e RELEASE
+          then cat RELEASE
+          else echo {{.RELEASE_FROM_DIR}} | tee RELEASE
+          fi
+        fi
+
+  OPERATOR_TAG: "{{.RELEASE}}"
 
   OPERATOR_IMG:
     sh: awk -F= '/ARG OPERATOR_IMAGE_DEFAULT=/ { print $2 ; exit }' 
olaris-op/Dockerfile
-  OPERATOR_TAG:
-    sh: git  -C olaris-op rev-parse --short HEAD
   OPERATOR_SRC: "{{.OPERATOR_IMG}}:{{.OPERATOR_TAG}}"
 
   STREAMER_TAG: "{{.OPERATOR_TAG}}"
@@ -43,7 +54,9 @@ vars:
 
 tasks:
 
-    default: task -l
+    default:
+      -  task -l
+      - echo release '{{.RELEASE}}'
 
     clean:
       desc: clean the intermediate artifacts
@@ -168,11 +181,11 @@ tasks:
       - test -e opsroot.orig || jq . <opsroot.json >opsroot.orig
       - >
         jq <opsroot.orig >opsroot.json  '
-          .config.images.operator = "{{.OPERATOR_SRC}}"   |
-          .config.images.streamer = "{{.STREAMER_SRC}}"   |
-          .config.images.systemapi = "{{.ADMIN_API_SRC}}" |
+          .config.images.operator   = "{{.OPERATOR_SRC}}"   |
+          .config.images.streamer   = "{{.STREAMER_SRC}}"   |
+          .config.images.systemapi  = "{{.ADMIN_API_SRC}}"  |
           .config.images.controller = "{{.CONTROLLER_SRC}}" |
-          .config.images.invoker = "{{.INVOKER_SRC}}"
+          .config.images.invoker    = "{{.INVOKER_SRC}}"
         '
       - diff opsroot.orig opsroot.json || true
 
@@ -193,10 +206,25 @@ tasks:
         then echo 'export PATH=~/.local/bin:"$PATH"' >>~/.profile
         fi
 
+    gitinit:
+      desc: initialize git if missing
+      cmds:
+      - |
+        for i in build cli olaris-op runtimes streamer admin-api
+        do
+          cd "$i"
+          if ! test -e .git
+          then git init 2>/dev/null
+              git commit -m "init" --allow-empty
+          fi
+          cd ..
+        done
 
     build:
       desc: build all
       cmds:
+      - python3 rebrander.py --do-it-for-real
+      - task: gitinit
       - task: opslink
       - task: openwhisk
       - task: operator
@@ -215,6 +243,8 @@ tasks:
           export OPS_BRANCH="{{.OPS_BRANCH}}"
           which ops
           ops -info
+          ops setup docker delete || true
+          touch .secrets
           ./all.sh kind
 
     license:
@@ -225,9 +255,25 @@ tasks:
         license-eye header {{.CMD}}
         for i in */.licenserc.yaml
         do
-            DIR=$(dirname $i)
+            DIR="$(dirname $i)"
             echo "*** Checking license headers in $DIR"
             cd $DIR
             license-eye header {{.CMD}}
             cd {{.TASKFILE_DIR}}
         done
+
+    release:
+      desc: release
+      requires:
+        vars: [ V ]
+      cmds:
+      - git tag "v{{.V}}"
+      - echo "v{{.V}}" >RELEASE
+      - python3 rebrander.py --do-it-for-real
+      - >
+          {{if eq OS "darwin"}}gtar{{else}}tar{{end}}
+          -C .. --exclude='.git'
+          --transform 's,^openserverless,openserverless-v{{.V}},'
+          -czvf ../openserverless-v{{.V}}-incubating-src.tar.gz
+          openserverless
+      - git push --tags
diff --git a/admin-api b/admin-api
index 1553fe4..634da39 160000
--- a/admin-api
+++ b/admin-api
@@ -1 +1 @@
-Subproject commit 1553fe44dee1a105b98c09507139aa3db0062b7e
+Subproject commit 634da39a750f7dc675c23ce0e64a9d8be7fc6b59
diff --git a/build b/build
index 7c97989..b2c9638 160000
--- a/build
+++ b/build
@@ -1 +1 @@
-Subproject commit 7c97989598e56be41fd5bb39e26b67183a5d210e
+Subproject commit b2c96380e1126dd0d372077b97c23b9e93ac9fea
diff --git a/build-and-test-ubuntu.sh b/build-and-test-ubuntu.sh
index 98e1220..08056fb 100755
--- a/build-and-test-ubuntu.sh
+++ b/build-and-test-ubuntu.sh
@@ -88,7 +88,7 @@ curl -fsSL 
"https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/${GOARCH}/kub
 sudo install -m 0755 /tmp/kubectl /usr/local/bin/kubectl
 rm -f /tmp/kubectl
 
-echo "Installing license-eye" 
+echo "Installing license-eye"
 go install github.com/apache/skywalking-eyes/cmd/license-eye@latest
 
 export PATH="$(go env GOPATH)/bin:/usr/local/bin:$PATH"
@@ -99,6 +99,10 @@ echo "Building and testing"
 cd "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
 git config --global --add safe.directory "$PWD"
 git config --global --add safe.directory "$PWD/olaris-op"
+
+git config --global --get user.name  >/dev/null || git config --global 
user.name  "Build OpenServerless"
+git config --global --get user.email >/dev/null || git config --global 
user.email "[email protected]"
+
 mkdir -p ~/.ssh
 task license
 task build
diff --git a/cli b/cli
index b0fc9da..3516d9a 160000
--- a/cli
+++ b/cli
@@ -1 +1 @@
-Subproject commit b0fc9da52997e591ed743068e5a1ba822872095e
+Subproject commit 3516d9af73a40439a0466199bdb4031cd7cc9984
diff --git a/olaris b/olaris
index 3a149de..795b702 160000
--- a/olaris
+++ b/olaris
@@ -1 +1 @@
-Subproject commit 3a149de862fc46f515e05957a1add868c2a777a2
+Subproject commit 795b7026c12d9332689b79e679d7152df01754f8
diff --git a/olaris-op b/olaris-op
index 7337d43..4710c01 160000
--- a/olaris-op
+++ b/olaris-op
@@ -1 +1 @@
-Subproject commit 7337d43fa7d4f16aff81c712c701805f758aa8e0
+Subproject commit 4710c0110dec6b9adc160a770e45b51ce9e1a90a
diff --git a/rebrander.md b/rebrander.md
new file mode 100644
index 0000000..7c6a931
--- /dev/null
+++ b/rebrander.md
@@ -0,0 +1,69 @@
+Write a rebrander.py python script with uv embedded deps.
+
+The goal is rebrand openserverless in openserverless
+
+It walks all the subfolders renaming with `git mv` all the folder 
openserverless in openserverless
+
+Then it will rename with `git mv` the files including "openserverless" in the 
name with the name including "openserverless"
+
+This file rename is mandatory, not optional: file names written inside other
+files are rebranded as plain text, so if the files themselves are not renamed
+the references point at paths that no longer exist. Rename folders first, then
+files, so each `git mv` targets a path that still exists. Dispatch each
+`git mv` to the repository that actually tracks the path, as with folders, and
+record a failure as a warning and skip that path.
+
+Log the renamed files the same way as the renamed folders.
+
+Then walks all the files replacing all the occurrenciens of
+- "openserverless" in "opensverveless"
+- "OpenServerless" in "OpenServerless"
+- "OPENSERVERLESS" to "OPENSERVERLESS"
+
+warn any usage of OpenServerless with other cases
+
+accept existing
+- OPENSERVERLESS
+- openserverless
+- Openserverless
+
+as common usages within the codebase,
+
+warn of differenct casing of openserverless and openserverless
+
+Leave a rebrander.log listing:
+
+- all the folders renamed
+- all the lines replaced
+- all the lines left as is
+
+show in the log for the lines: <file>:<line>:<newvalue>
+
+include in the log the excluded lines with prefix SKIP:<file>:<line>:<oldvalue>
+
+By default is it in dry run model.
+
+Execute with "--do-it-for-real"
+
+Exclusion rules:
+
+All the files:
+- .gitmodules
+
+All the lines:
+
+- lines  matching  "OPenServerless"
+- lines matchins nuvolaris-testing
+- lines matching registry.hub.docker.com/nuvolaris/
+- lines with @nuvolaris.io (email)
+- lines with ghcr.io/nuvolaris/ (ghcr.io references)
+- .go, go.mod and go.sum files matching "github.com/openserverless/*" (github 
repo references)
+- lines with assets-global.website-files.com (hosted CDN asset keys: renaming
+  the URL does not rename the remote object). Only the URL is protected; the
+  surrounding text on the same line, such as alt text, must still be rebranded.
+- file build-step.md
+- file olaris/prereq.yml
+
+The same exclusion rules apply when the script is run inside a submodule
+(olaris-op, olaris, testing), which the top-level walk does not enter.
+
diff --git a/rebrander.py b/rebrander.py
new file mode 100755
index 0000000..8a0a200
--- /dev/null
+++ b/rebrander.py
@@ -0,0 +1,451 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Rebrand "nuvolaris" to "openserverless".
+
+Walks all subfolders renaming every folder named with "nuvolaris" in it,
+then every file whose name carries it (both with `git mv`, so the rename is
+staged rather than looking like a delete plus an untracked copy). Renaming
+the files is required, not cosmetic: a file name written inside another file
+is rebranded as plain text, so the reference would otherwise point at a path
+that no longer exists. Then walks all files replacing:
+
+    nuvolaris -> openserverless
+    Nuvolaris -> OpenServerless
+    NUVOLARIS -> OPENSERVERLESS
+
+Any *other* casing (nuVolaris, NuVoLaRiS, ...) is NOT rewritten: it is
+reported as a warning, in the log and on stderr, for a human to handle.
+Pre-existing casings of the new name are accepted without warning when
+they are one of the three common forms (openserverless, OpenServerless,
+OPENSERVERLESS, plus Openserverless); anything else is warned about,
+except the known "OPenServerless" spelling, whose lines are skipped.
+
+A few files are excluded wholesale (.gitmodules anywhere, plus build-step.md
+and olaris/prereq.yml, which carry upstream github URLs that must keep
+resolving). Lines naming external resources that still exist under the old
+name (nuvolaris-testing, registry.hub.docker.com/nuvolaris/, ghcr.io and
+github.com repo references, @nuvolaris.io addresses) are left as they are.
+
+Writes rebrander.log listing the folders and files renamed, every replaced 
line as
+<file>:<line>:<newvalue>, and every line left as is by an exclusion rule
+as SKIP:<file>:<line>:<oldvalue>.
+
+By default nothing is modified: the script only reports what it would do.
+Pass --do-it-for-real to actually rename folders and files and rewrite them.
+
+Usage:
+    ./rebrander.py [ROOT] [--do-it-for-real]
+"""
+
+import argparse
+import os
+import re
+import subprocess
+import sys
+
+OLD = "nuvolaris"
+NEW = "openserverless"
+
+# The only two casings that get rewritten; everything else is warned about.
+REPLACEMENTS = {
+    "nuvolaris": "openserverless",
+    "Nuvolaris": "OpenServerless",
+    "NUVOLARIS": "OPENSERVERLESS",
+}
+
+# Casings of the new name accepted as common usage; anything else is warned.
+ACCEPTED_NEW = {"openserverless", "OpenServerless", "OPENSERVERLESS",
+                "Openserverless"}
+
+LOG_NAME = "rebrander.log"
+
+# Never descend into these; renaming/rewriting them corrupts the checkout.
+SKIP_DIRS = {".git", ".hg", ".svn", "node_modules", "__pycache__",
+             ".venv", "venv", ".mypy_cache", ".pytest_cache", ".ruff_cache"}
+
+# Excluded files (spec): .gitmodules holds submodule URLs. Matched by base
+# name, so nested submodule files are covered too.
+SKIP_FILES = {".gitmodules"}
+
+# Excluded files given as a path relative to the root: unlike the above these
+# name one specific file, not every file that happens to share the base name
+# (there are several prereq.yml in the tree; only olaris/ is excluded).
+SKIP_PATHS = {"build-step.md", os.path.join("olaris", "prereq.yml")}
+
+# This script and its own log must not rewrite themselves mid-run.
+SELF_FILES = {LOG_NAME, os.path.basename(__file__)}
+
+# Excluded lines (spec). These are left byte-for-byte as they are and logged
+# under SKIP. The first three apply to every file; the github repo reference
+# is excluded only inside .go files.
+ODD_NEW_RE = re.compile(r"OPenServerless")
+EMAIL_RE = re.compile(r"@nuvolaris\.io", re.IGNORECASE)
+GHCR_RE = re.compile(r"ghcr\.io/nuvolaris/", re.IGNORECASE)
+GO_REPO_RE = re.compile(r"github\.com/nuvolaris/", re.IGNORECASE)
+# Live external resources registered under the old name: a GCP project /
+# cluster and Docker Hub images. Renaming the string does not rename them.
+TESTING_RE = re.compile(r"nuvolaris-testing", re.IGNORECASE)
+DOCKERHUB_RE = re.compile(r"registry\.hub\.docker\.com/nuvolaris/", 
re.IGNORECASE)
+
+# Go module files pin the same repo references as the .go sources do.
+GO_MODULE_FILES = {"go.mod", "go.sum"}
+
+ANY_FILE_EXCLUDES = (ODD_NEW_RE, TESTING_RE, DOCKERHUB_RE, EMAIL_RE, GHCR_RE)
+
+MAX_BYTES = 20 * 1024 * 1024  # skip anything bigger; not source text
+
+OLD_ANY = re.compile(OLD, re.IGNORECASE)
+NEW_ANY = re.compile(NEW, re.IGNORECASE)
+
+
+def substitute(text: str):
+    """Replace the two accepted casings.
+
+    Returns (new_text, [odd casings of OLD left untouched]).
+    """
+    odd = []
+
+    def repl(m):
+        word = m.group(0)
+        if word in REPLACEMENTS:
+            return REPLACEMENTS[word]
+        odd.append(word)   # unknown casing: leave it alone, warn instead
+        return word
+
+    return OLD_ANY.sub(repl, text), odd
+
+
+def odd_new_casings(text: str):
+    """Occurrences of the new name in a casing we don't consider canonical."""
+    return [m.group(0) for m in NEW_ANY.finditer(text)
+            if m.group(0) not in ACCEPTED_NEW]
+
+
+def line_excluded(path: str, line: str) -> bool:
+    """Spec: skip lines carrying "OPenServerless", a nuvolaris-testing or
+    registry.hub.docker.com/nuvolaris/ reference, a @nuvolaris.io address or
+    a ghcr.io/nuvolaris/ reference, plus github.com/nuvolaris/ inside the go
+    sources and the module files that pin them."""
+    if any(rx.search(line) for rx in ANY_FILE_EXCLUDES):
+        return True
+    base = os.path.basename(path)
+    if not (path.endswith(".go") or base in GO_MODULE_FILES):
+        return False
+    return GO_REPO_RE.search(line) is not None
+
+
+def is_binary(path: str) -> bool:
+    try:
+        with open(path, "rb") as f:
+            chunk = f.read(8000)
+    except OSError:
+        return True
+    return b"\0" in chunk
+
+
+def prune(dirnames):
+    dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
+
+
+def git_toplevel(path: str):
+    """Repo that tracks `path`, or None if it is not under git control.
+
+    Submodules are separate repositories: a `git mv` has to run inside the one
+    that actually tracks the directory, not the outermost checkout.
+    """
+    try:
+        out = subprocess.run(["git", "-C", path, "rev-parse", 
"--show-toplevel"],
+                             capture_output=True, text=True, check=True)
+    except (OSError, subprocess.CalledProcessError):
+        return None
+    return out.stdout.strip() or None
+
+
+def git_mv(old_path: str, new_path: str):
+    """Move with `git mv` so the rename is staged, not seen as 
delete+untracked.
+
+    Returns None on success, else a message. This matters because the run also
+    rewrites .gitignore rules that name the very directories being renamed: an
+    unstaged destination can be swallowed by the rewritten rule and silently
+    dropped from the tree.
+
+    Used for both folders and files; git resolves the destination against the
+    repo that tracks the source, so nested submodules are handled correctly.
+    """
+    repo = git_toplevel(os.path.dirname(old_path))
+    if repo is None:
+        return "not a git repository"
+    try:
+        r = subprocess.run(["git", "-C", repo, "mv", old_path, new_path],
+                           capture_output=True, text=True)
+    except OSError as e:
+        return str(e)
+    if r.returncode == 0:
+        return None
+    return (r.stderr or r.stdout).strip() or f"git mv exited {r.returncode}"
+
+
+def rename_folders(root: str, dry_run: bool, warnings):
+    """Rename matching directories with `git mv`, deepest-first.
+
+    Deepest-first keeps every stored path valid: renaming a parent before its
+    child would invalidate the child's path.
+    """
+    renamed = []
+    targets = []
+    for dirpath, dirnames, _ in os.walk(root):
+        prune(dirnames)
+        for d in dirnames:
+            if OLD_ANY.search(d):
+                targets.append(os.path.join(dirpath, d))
+
+    for old_path in sorted(targets, key=lambda p: p.count(os.sep), 
reverse=True):
+        parent, name = os.path.split(old_path)
+        new_name, odd = substitute(name)
+        rel_old = os.path.relpath(old_path, root)
+        for w in odd:
+            warnings.append((rel_old, 0, f"folder name has unhandled casing 
{w!r}"))
+        if new_name == name:
+            continue
+        new_path = os.path.join(parent, new_name)
+        if os.path.exists(new_path):
+            warnings.append((rel_old, 0, f"rename target already exists: 
{new_name!r}"))
+            continue
+        if not dry_run:
+            err = git_mv(old_path, new_path)
+            if err:
+                warnings.append((rel_old, 0, f"git mv failed: {err}"))
+                continue
+        renamed.append((rel_old, os.path.relpath(new_path, root)))
+    return renamed
+
+
+def rename_files(root: str, dry_run: bool, warnings):
+    """Rename matching files with `git mv`.
+
+    Runs after rename_folders so every path here is the post-rename one; a
+    file inside a renamed directory is reached under its new parent.
+
+    This is not optional: file names written inside other files are rebranded
+    as plain text, so leaving the files themselves alone would point those
+    references at paths that no longer exist.
+    """
+    renamed = []
+    targets = []
+    for dirpath, dirnames, filenames in os.walk(root):
+        prune(dirnames)
+        for fn in filenames:
+            if fn in SELF_FILES:
+                continue
+            if OLD_ANY.search(fn):
+                targets.append(os.path.join(dirpath, fn))
+
+    for old_path in sorted(targets):
+        parent, name = os.path.split(old_path)
+        new_name, odd = substitute(name)
+        rel_old = os.path.relpath(old_path, root)
+        for w in odd:
+            warnings.append((rel_old, 0, f"file name has unhandled casing 
{w!r}"))
+        if new_name == name:
+            continue
+        if os.path.islink(old_path):
+            warnings.append((rel_old, 0, "symlink not renamed"))
+            continue
+        new_path = os.path.join(parent, new_name)
+        if os.path.exists(new_path):
+            warnings.append((rel_old, 0, f"rename target already exists: 
{new_name!r}"))
+            continue
+        if not dry_run:
+            err = git_mv(old_path, new_path)
+            if err:
+                warnings.append((rel_old, 0, f"git mv failed: {err}"))
+                continue
+        renamed.append((rel_old, os.path.relpath(new_path, root)))
+    return renamed
+
+
+def rewrite_files(root: str, dry_run: bool, warnings):
+    """Replace occurrences inside files; return per-file changed line pairs."""
+    changes = []  # (relpath, [(lineno, newvalue), ...])
+    skipped = []  # (relpath, [(lineno, oldvalue), ...]) - excluded by a rule
+    for dirpath, dirnames, filenames in os.walk(root):
+        prune(dirnames)
+        for fn in filenames:
+            if fn in SELF_FILES or fn in SKIP_FILES:
+                continue
+            path = os.path.join(dirpath, fn)
+            rel = os.path.relpath(path, root)
+            if rel in SKIP_PATHS:
+                continue
+            if os.path.islink(path):
+                continue
+            try:
+                if os.path.getsize(path) > MAX_BYTES:
+                    continue
+            except OSError:
+                continue
+            if is_binary(path):
+                continue
+            try:
+                with open(path, "r", encoding="utf-8", newline="") as f:
+                    lines = f.readlines()
+            except (UnicodeDecodeError, OSError):
+                continue
+
+            hits = []
+            skips = []
+            out = []
+            for i, line in enumerate(lines, 1):
+                if line_excluded(rel, line):
+                    out.append(line)
+                    # Only worth reporting if the rule actually held something
+                    # back; an excluded line with nothing to replace is noise.
+                    if substitute(line)[0] != line:
+                        skips.append((i, line.rstrip("\r\n")))
+                    continue
+
+                new_line, odd = substitute(line)
+                out.append(new_line)
+
+                for w in odd:
+                    warnings.append((rel, i, f"unhandled casing {w!r} left 
as-is"))
+                for w in odd_new_casings(line):
+                    warnings.append((rel, i, f"non-canonical {NEW!r} casing 
{w!r}"))
+
+                if new_line != line:
+                    hits.append((i, new_line.rstrip("\r\n")))
+
+            if skips:
+                skipped.append((rel, skips))
+            if not hits:
+                continue
+            if not dry_run:
+                try:
+                    with open(path, "w", encoding="utf-8", newline="") as f:
+                        f.writelines(out)
+                except OSError as e:
+                    warnings.append((rel, 0, f"write failed: {e}"))
+                    continue
+            changes.append((rel, hits))
+    return changes, skipped
+
+
+def write_log(root: str, renamed, renamed_files, changes, skipped, warnings,
+              dry_run: bool):
+    log_path = os.path.join(root, LOG_NAME)
+    total_lines = sum(len(h) for _, h in changes)
+    total_skipped = sum(len(h) for _, h in skipped)
+    bar = "=" * 70
+    with open(log_path, "w", encoding="utf-8") as log:
+        log.write(f"rebrander: {OLD} -> {NEW}\n")
+        log.write(f"root: {os.path.abspath(root)}\n")
+        if dry_run:
+            log.write("mode: DRY RUN - nothing modified "
+                      "(pass --do-it-for-real to apply)\n")
+        else:
+            log.write("mode: APPLIED - changes written to disk\n")
+        log.write(f"folders renamed: {len(renamed)}\n")
+        log.write(f"files renamed:   {len(renamed_files)}\n")
+        log.write(f"files changed:   {len(changes)}\n")
+        log.write(f"lines replaced:  {total_lines}\n")
+        log.write(f"lines left as is:{total_skipped}\n")
+        log.write(f"warnings:        {len(warnings)}\n")
+
+        log.write(f"\n{bar}\nFOLDERS RENAMED\n{bar}\n")
+        if renamed:
+            for old, new in renamed:
+                log.write(f"  {old}\n-> {new}\n")
+        else:
+            log.write("  (none)\n")
+
+        log.write(f"\n{bar}\nFILES RENAMED\n{bar}\n")
+        if renamed_files:
+            for old_rel, new_rel in renamed_files:
+                log.write(f"  {old_rel}\n-> {new_rel}\n")
+        else:
+            log.write("  (none)\n")
+
+        log.write(f"\n{bar}\nWARNINGS\n{bar}\n")
+        if warnings:
+            for rel, lineno, msg in sorted(warnings):
+                where = f"{rel}:{lineno}" if lineno else rel
+                log.write(f"  {where}: {msg}\n")
+        else:
+            log.write("  (none)\n")
+
+        log.write(f"\n{bar}\nLINES REPLACED\n{bar}\n")
+        if changes:
+            for relpath, hits in sorted(changes):
+                for lineno, after in hits:
+                    log.write(f"{relpath}:{lineno}:{after}\n")
+        else:
+            log.write("  (none)\n")
+
+        log.write(f"\n{bar}\nLINES LEFT AS IS (excluded)\n{bar}\n")
+        if skipped:
+            for relpath, hits in sorted(skipped):
+                for lineno, before in hits:
+                    log.write(f"SKIP:{relpath}:{lineno}:{before}\n")
+        else:
+            log.write("  (none)\n")
+    return log_path, total_lines, total_skipped
+
+
+def main():
+    ap = argparse.ArgumentParser(description=f"Rebrand {OLD} -> {NEW}")
+    ap.add_argument("root", nargs="?", default=".", help="root folder 
(default: .)")
+    ap.add_argument("--do-it-for-real", action="store_true", dest="for_real",
+                    help="actually rename and rewrite; without it nothing is "
+                         "modified and only the log is produced")
+    args = ap.parse_args()
+    dry_run = not args.for_real
+
+    root = os.path.abspath(args.root)
+    if not os.path.isdir(root):
+        sys.exit(f"not a directory: {root}")
+
+    warnings = []
+    # Folders first, then files, so every `git mv` targets a path that still
+    # exists; the file walk then sees the final paths in the log.
+    renamed = rename_folders(root, dry_run, warnings)
+    renamed_files = rename_files(root, dry_run, warnings)
+    changes, skipped = rewrite_files(root, dry_run, warnings)
+    log_path, total_lines, total_skipped = write_log(
+        root, renamed, renamed_files, changes, skipped, warnings, dry_run)
+
+    for rel, lineno, msg in sorted(warnings)[:20]:
+        where = f"{rel}:{lineno}" if lineno else rel
+        print(f"WARN {where}: {msg}", file=sys.stderr)
+    if len(warnings) > 20:
+        print(f"WARN ... and {len(warnings) - 20} more (see {LOG_NAME})",
+              file=sys.stderr)
+
+    print(f"folders renamed: {len(renamed)}")
+    print(f"files renamed:   {len(renamed_files)}")
+    print(f"files changed:   {len(changes)}")
+    print(f"lines replaced:  {total_lines}")
+    print(f"lines left as is:{total_skipped}")
+    print(f"warnings:        {len(warnings)}")
+    print(f"log: {log_path}")
+    if dry_run:
+        print("(dry run - nothing was modified; "
+              "pass --do-it-for-real to apply)")
+
+
+if __name__ == "__main__":
+    main()
diff --git a/runtimes b/runtimes
index e09c5bc..d86fcf0 160000
--- a/runtimes
+++ b/runtimes
@@ -1 +1 @@
-Subproject commit e09c5bc7d256af5f8fcc36cc65413d34a87ef090
+Subproject commit d86fcf08f71037f908dbcd8da0e24dc4df7a754a
diff --git a/testing b/testing
index bd633c2..e8e68ff 160000
--- a/testing
+++ b/testing
@@ -1 +1 @@
-Subproject commit bd633c2136afc4f1b1c3d8ee00ccc9babc42dda2
+Subproject commit e8e68ff5dfb9b0b2b4153889d0c5f7e6885b5908

Reply via email to