Skip to content

Commit 19fb9b0

Browse files
committed
allow specifying target branch
1 parent 40f91a5 commit 19fb9b0

2 files changed

Lines changed: 164 additions & 89 deletions

File tree

monorepo-migration/migrate.sh

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ echo "Starting migration using git read-tree with isolated clones..."
6262
# 0. Create working directory
6363
mkdir -p "$WORKING_DIR"
6464

65+
MIGRATION_HEAD_BRANCH="add-migration-script"
66+
6567
# 1. Clone the source repository
6668
if [ ! -d "$SOURCE_DIR" ]; then
6769
echo "Cloning source repo: $SOURCE_REPO_URL into $SOURCE_DIR"
@@ -70,7 +72,7 @@ else
7072
echo "Source directory $SOURCE_DIR already exists. Ensuring it is clean and up-to-date..."
7173
cd "$SOURCE_DIR"
7274
git fetch origin
73-
git checkout -f main
75+
git checkout -f "main"
7476
git reset --hard origin/main
7577
git clean -fd
7678
cd - > /dev/null
@@ -109,12 +111,14 @@ fi
109111
if [ ! -d "$TARGET_DIR" ]; then
110112
echo "Cloning target monorepo: $MONOREPO_URL into $TARGET_DIR"
111113
git clone "$MONOREPO_URL" "$TARGET_DIR"
114+
git checkout -f "${MIGRATION_HEAD_BRANCH}"
115+
git reset --hard origin/${MIGRATION_HEAD_BRANCH}
112116
else
113117
echo "Target directory $TARGET_DIR already exists. Ensuring it is clean and up-to-date..."
114118
cd "$TARGET_DIR"
115119
git fetch origin
116-
git checkout -f main
117-
git reset --hard origin/main
120+
git checkout -f "${MIGRATION_HEAD_BRANCH}"
121+
git reset --hard origin/${MIGRATION_HEAD_BRANCH}
118122
git clean -fd
119123
cd - > /dev/null
120124
fi
@@ -126,8 +130,8 @@ echo "Ensuring clean state in target monorepo..."
126130
git fetch origin
127131
git reset --hard HEAD
128132
git clean -fd
129-
git checkout -f main
130-
git reset --hard origin/main
133+
git checkout -f "${MIGRATION_HEAD_BRANCH}"
134+
git reset --hard origin/${MIGRATION_HEAD_BRANCH}
131135
git clean -fdx
132136

133137
# Check if the repository is already migrated
@@ -327,7 +331,7 @@ git commit -n --no-gpg-sign -m "chore($SOURCE_REPO_NAME): modernize root pom.xml
327331
# 7.12 Modernize BOM pom.xml
328332
echo "Modernizing BOM pom.xml..."
329333
# Find potential BOM POMs (usually in a subdirectory ending with -bom)
330-
find "$SOURCE_REPO_NAME" -name "pom.xml" | grep "\-bom/pom.xml" | while read -r bom_pom; do
334+
find "$SOURCE_REPO_NAME" -name "pom.xml" | grep "\-bom/pom.xml" | grep -v "samples" | while read -r bom_pom; do
331335
echo "Modernizing BOM: $bom_pom"
332336
# BOMs should inherit from google-cloud-pom-parent
333337
python3 "$MODERNIZE_POM_SCRIPT" "$bom_pom" "$PARENT_VERSION" "$SOURCE_REPO_NAME" "google-cloud-pom-parent" "../../google-cloud-pom-parent/pom.xml"

monorepo-migration/update_owlbot.py

Lines changed: 154 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -16,55 +16,122 @@
1616
import sys
1717
import os
1818

19+
def is_call_to(node, name_parts):
20+
"""
21+
Checks if an AST node is a call to a specific function.
22+
name_parts: list of strings, e.g. ['s', 'move'] for s.move()
23+
or ['java', 'common_templates']
24+
"""
25+
if not isinstance(node, ast.Call):
26+
return False
27+
28+
func = node.func
29+
# Handle attribute access (e.g. s.move)
30+
if isinstance(func, ast.Attribute):
31+
if len(name_parts) == 2:
32+
# Check object and attr
33+
obj = func.value
34+
if isinstance(obj, ast.Name) and obj.id == name_parts[0] and func.attr == name_parts[1]:
35+
return True
36+
elif len(name_parts) == 1:
37+
if func.attr == name_parts[0]:
38+
return True
39+
# Handle direct name (e.g. if imported directly, though less common for these)
40+
elif isinstance(func, ast.Name):
41+
if len(name_parts) == 1 and func.id == name_parts[0]:
42+
return True
43+
44+
return False
45+
46+
def extract_excludes_from_call(call_node):
47+
excludes = []
48+
for keyword in call_node.keywords:
49+
if keyword.arg == 'excludes':
50+
if isinstance(keyword.value, ast.List):
51+
for elt in keyword.value.elts:
52+
if isinstance(elt, ast.Constant): # Python 3.8+
53+
excludes.append(elt.value)
54+
elif isinstance(elt, ast.Str): # Python < 3.8
55+
excludes.append(elt.s)
56+
break
57+
return excludes
58+
1959
def extract_info(source_code):
20-
"""Extracts excludes and replacement calls from the source owlbot.py."""
2160
excludes = []
22-
replacements = []
23-
61+
loop_body_lines = []
62+
top_level_lines = []
63+
2464
try:
2565
tree = ast.parse(source_code)
2666
except SyntaxError:
27-
return excludes, replacements
28-
29-
for node in ast.walk(tree):
30-
# Look for s.replace(...) calls
31-
if isinstance(node, ast.Call):
32-
if isinstance(node.func, ast.Attribute) and node.func.attr == 'replace':
33-
# We found a replace call. We want to reconstruct the source code for this call.
34-
# However, ast.unparse is only available in Python 3.9+.
35-
# If we are on an older python, we might need a workaround or just grab the line range.
36-
# Since we likely have 3.9+, let's try ast.unparse if available, otherwise strict extraction.
37-
if sys.version_info >= (3, 9):
38-
replacements.append(ast.unparse(node))
39-
else:
40-
# Fallback for older python: just comment it needs manual migration or try simple extraction
41-
# For now assume 3.9+ which is standard in this environment
42-
pass
43-
44-
# Look for java.common_templates or common.java_library calls to find excludes
45-
if isinstance(node, ast.Call):
46-
is_common_templates = False
47-
if isinstance(node.func, ast.Attribute):
48-
if node.func.attr == 'common_templates':
49-
is_common_templates = True
50-
elif node.func.attr == 'java_library': # older name
51-
is_common_templates = True
67+
return excludes, top_level_lines, loop_body_lines
68+
69+
for node in tree.body:
70+
# Some nodes are wrapped in Expr, e.g. s.remove_staging_dirs()
71+
inner_node = node
72+
if isinstance(node, ast.Expr):
73+
inner_node = node.value
74+
75+
# Ignore standard imports (we will inject them)
76+
if isinstance(node, (ast.Import, ast.ImportFrom)):
77+
# We assume we only care about synthtool/java imports which we regenerate.
78+
# If there are other imports, we should probably preserve them.
79+
# Heuristic: if it mentions 'synthtool', ignore it.
80+
if isinstance(node, ast.Import):
81+
if any('synthtool' in alias.name for alias in node.names):
82+
continue
83+
if isinstance(node, ast.ImportFrom):
84+
if node.module and 'synthtool' in node.module:
85+
continue
86+
# Preserve other imports
87+
if sys.version_info >= (3, 9):
88+
top_level_lines.append(ast.unparse(node))
89+
continue
90+
91+
# Check for java.common_templates (top level)
92+
if is_call_to(inner_node, ['java', 'common_templates']) or is_call_to(inner_node, ['common', 'java_library']):
93+
excludes.extend(extract_excludes_from_call(inner_node))
94+
continue
95+
96+
# Check for s.remove_staging_dirs()
97+
if is_call_to(inner_node, ['s', 'remove_staging_dirs']):
98+
continue
99+
100+
# Check for the main loop: for library in s.get_staging_dirs():
101+
if isinstance(node, ast.For):
102+
is_staging_loop = False
103+
if isinstance(node.iter, ast.Call):
104+
# Check for s.get_staging_dirs()
105+
if is_call_to(node.iter, ['s', 'get_staging_dirs']):
106+
is_staging_loop = True
52107

53-
if is_common_templates:
54-
for keyword in node.keywords:
55-
if keyword.arg == 'excludes':
56-
if isinstance(keyword.value, ast.List):
57-
for elt in keyword.value.elts:
58-
if isinstance(elt, ast.Constant): # Python 3.8+
59-
excludes.append(elt.value)
60-
elif isinstance(elt, ast.Str): # Python < 3.8
61-
excludes.append(elt.s)
62-
break
63-
return excludes, replacements
64-
65-
66-
def generate_target_content(excludes, replacements, standard_excludes=None):
67-
# Default excludes for monorepo if no template is provided
108+
if is_staging_loop:
109+
# Extract body
110+
for child in node.body:
111+
child_inner = child
112+
if isinstance(child, ast.Expr):
113+
child_inner = child.value
114+
115+
# Ignore s.move(library)
116+
if is_call_to(child_inner, ['s', 'move']):
117+
continue
118+
# Check for nested common_templates (rare but possible)
119+
if is_call_to(child_inner, ['java', 'common_templates']) or is_call_to(child_inner, ['common', 'java_library']):
120+
excludes.extend(extract_excludes_from_call(child_inner))
121+
continue
122+
123+
if sys.version_info >= (3, 9):
124+
loop_body_lines.append(ast.unparse(child))
125+
continue
126+
# else fall through to preserve other loops
127+
128+
# Preserve everything else (constants, functions, other logic)
129+
if sys.version_info >= (3, 9):
130+
top_level_lines.append(ast.unparse(node))
131+
132+
return excludes, top_level_lines, loop_body_lines
133+
134+
def generate_target_content(excludes, top_level_lines, loop_body_lines, standard_excludes=None):
68135
if standard_excludes is None:
69136
standard_excludes = {
70137
".github/*",
@@ -80,44 +147,47 @@ def generate_target_content(excludes, replacements, standard_excludes=None):
80147
".gitignore"
81148
}
82149

83-
# Merge excludes
84-
# User requested to ignore source excludes and strictly use standard excludes
85150
final_excludes = sorted(list(set(standard_excludes)))
86-
87-
# Format replacements with indentation
88-
formatted_replacements = ""
89-
for rep in replacements:
90-
formatted_replacements += " " + rep + "\n"
91-
92-
excludes_str = ",\n ".join([f'"{e}"' for e in final_excludes])
93-
94-
content = f"""# Copyright 2026 Google LLC
95-
#
96-
# Licensed under the Apache License, Version 2.0 (the "License");
97-
# you may not use this file except in compliance with the License.
98-
# You may obtain a copy of the License at
99-
#
100-
# https://www.apache.org/licenses/LICENSE-2.0
101-
#
102-
# Unless required by applicable law or agreed to in writing, software
103-
# distributed under the License is distributed on an "AS IS" BASIS,
104-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
105-
# See the License for the specific language governing permissions and
106-
# limitations under the License.
151+
excludes_str = ",\n ".join([f'"{e}"' for e in final_excludes])
107152

108-
import synthtool as s
109-
from synthtool.languages import java
153+
# Reconstruct content
154+
lines = []
155+
lines.append("# Copyright 2026 Google LLC")
156+
lines.append("#")
157+
lines.append("# Licensed under the Apache License, Version 2.0 (the \"License\");")
158+
lines.append("# you may not use this file except in compliance with the License.")
159+
lines.append("# You may obtain a copy of the License at")
160+
lines.append("#")
161+
lines.append("# https://www.apache.org/licenses/LICENSE-2.0")
162+
lines.append("#")
163+
lines.append("# Unless required by applicable law or agreed to in writing, software")
164+
lines.append("# distributed under the License is distributed on an \"AS IS\" BASIS,")
165+
lines.append("# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.")
166+
lines.append("# See the License for the specific language governing permissions and")
167+
lines.append("# limitations under the License.")
168+
lines.append("")
169+
lines.append("import synthtool as s")
170+
lines.append("from synthtool.languages import java")
171+
lines.append("")
172+
173+
if top_level_lines:
174+
lines.extend(top_level_lines)
175+
lines.append("")
110176

111-
for library in s.get_staging_dirs():
112-
# put any special-case replacements here
113-
s.move(library)
114-
{formatted_replacements}
115-
s.remove_staging_dirs()
116-
java.common_templates(monorepo=True, excludes=[
117-
{excludes_str}
118-
])
119-
"""
120-
return content
177+
lines.append("for library in s.get_staging_dirs():")
178+
lines.append(" # put any special-case replacements here")
179+
lines.append(" s.move(library)")
180+
for l in loop_body_lines:
181+
# Indent loop body
182+
for sl in l.split('\n'):
183+
lines.append(" " + sl)
184+
185+
lines.append("s.remove_staging_dirs()")
186+
lines.append(f"java.common_templates(monorepo=True, excludes=[")
187+
lines.append(f" {excludes_str}")
188+
lines.append("])")
189+
190+
return "\n".join(lines) + "\n"
121191

122192
def main():
123193
if len(sys.argv) < 3:
@@ -135,21 +205,22 @@ def main():
135205
with open(source_file, 'r') as f:
136206
source_code = f.read()
137207

138-
excludes, replacements = extract_info(source_code)
208+
excludes, top_level_lines, loop_body_lines = extract_info(source_code)
139209

140210
standard_excludes = None
141211
if template_file:
142212
if os.path.exists(template_file):
143213
with open(template_file, 'r') as f:
144214
template_code = f.read()
145-
template_excludes, _ = extract_info(template_code)
215+
template_excludes, _, _ = extract_info(template_code)
146216
standard_excludes = template_excludes
147217
else:
148-
print(f"Template file {template_file} not found. using default excludes.")
218+
print(f"Template file {template_file} not found using default excludes.")
149219

150-
target_content = generate_target_content(excludes, replacements, standard_excludes)
220+
target_content = generate_target_content(excludes, top_level_lines, loop_body_lines, standard_excludes)
151221

152-
os.makedirs(os.path.dirname(target_file), exist_ok=True)
222+
if os.path.dirname(target_file):
223+
os.makedirs(os.path.dirname(target_file), exist_ok=True)
153224
with open(target_file, 'w') as f:
154225
f.write(target_content)
155226

0 commit comments

Comments
 (0)