Skip to content

Commit b1a2312

Browse files
committed
Add single-updater
1 parent ca95052 commit b1a2312

File tree

2 files changed

+253
-0
lines changed

2 files changed

+253
-0
lines changed

Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ RUN printf "echo 'A zsh config is provided at ~/zshrc, move it to ~/.zshrc to ha
3333
RUN mkdir -p ~/.config && echo -e "cache:\n quota: 50G" > ~/.config/buildstream.conf
3434
RUN cd /home/${user}/build-root && ./makepkg.sh || true
3535
RUN sudo install -Dm0755 /home/${user}/build-root/abicheck.sh /usr/bin/abicheck
36+
RUN sudo install -Dm0755 /home/${user}/build-root/single-updater.py /usr/bin/single-updater
3637

3738
RUN sudo pip install --break-system-packages git+https://gitlab.com/BuildStream/infrastructure/gitlab-merge-request-generator.git
3839
RUN sudo pip install --break-system-packages git+https://gitlab.com/CodethinkLabs/lorry/bst-to-lorry.git

single-updater.py

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
#!/usr/bin/env python3
2+
"""Usage: single-updater --base-branch BRANCH --element ELEMENT"""
3+
4+
import re
5+
import subprocess
6+
import argparse
7+
import logging
8+
import shutil
9+
import datetime
10+
11+
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
12+
13+
14+
def is_cmd(cmd):
15+
return shutil.which(cmd) is not None
16+
17+
18+
def is_git_dir():
19+
return (
20+
subprocess.run(
21+
["git", "rev-parse"],
22+
stdout=subprocess.DEVNULL,
23+
stderr=subprocess.DEVNULL,
24+
check=False,
25+
).returncode
26+
== 0
27+
)
28+
29+
30+
def is_dirty():
31+
result = subprocess.run(
32+
["git", "status", "--porcelain"],
33+
stdout=subprocess.PIPE,
34+
stderr=subprocess.PIPE,
35+
check=False,
36+
text=True,
37+
)
38+
return bool(result.stdout.strip())
39+
40+
41+
def get_local_branches():
42+
result = subprocess.run(
43+
["git", "branch"],
44+
stdout=subprocess.PIPE,
45+
stderr=subprocess.PIPE,
46+
text=True,
47+
check=False,
48+
)
49+
return [line.strip().lstrip("* ").strip() for line in result.stdout.splitlines()]
50+
51+
52+
def delete_branch(branch):
53+
return (
54+
subprocess.run(
55+
["git", "branch", "-D", branch],
56+
stdout=subprocess.DEVNULL,
57+
stderr=subprocess.DEVNULL,
58+
check=False,
59+
).returncode
60+
== 0
61+
)
62+
63+
64+
def run_updater(branch, element):
65+
subprocess.run(
66+
[
67+
"auto_updater",
68+
f"--base_branch={branch}",
69+
"--nobuild",
70+
"--overwrite",
71+
"--push",
72+
"--shuffle-branches",
73+
"--on_track_error=continue",
74+
element,
75+
],
76+
stdout=subprocess.DEVNULL,
77+
stderr=subprocess.DEVNULL,
78+
check=False,
79+
)
80+
# It fails due to too many random tracking errors
81+
# that aren't handled upstream. But it can still create
82+
# branches with updates as it goes which are useful
83+
return True
84+
85+
86+
def create_branch(base_branch):
87+
timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
88+
branch_name = f"updates/{timestamp}"
89+
success = (
90+
subprocess.run(
91+
["git", "checkout", "-b", branch_name, base_branch],
92+
check=False,
93+
stdout=subprocess.PIPE,
94+
stderr=subprocess.PIPE,
95+
).returncode
96+
== 0
97+
)
98+
return branch_name if success else None
99+
100+
101+
def reformat_commit_message(commit_message):
102+
pattern = r"Update elements/(components|include|abi|bootstrap|extensions)/(.*?)[.](bst|yml) to (.*)"
103+
match = re.match(pattern, commit_message)
104+
if match:
105+
element_name = match.group(2)
106+
updated_version = match.group(4)
107+
return f"{element_name}: Update to {updated_version}"
108+
else:
109+
return commit_message
110+
111+
112+
def cherry_pick_top_commit(branches, new_branch):
113+
all_successful = True
114+
for branch in branches:
115+
checkout_branch(branch)
116+
result = subprocess.run(
117+
["git", "log", "--format=%H", "-n", "1"],
118+
stdout=subprocess.PIPE,
119+
stderr=subprocess.PIPE,
120+
check=False,
121+
text=True,
122+
)
123+
top_commit = result.stdout.strip()
124+
if top_commit:
125+
checkout_branch(new_branch)
126+
subprocess.run(
127+
["git", "cherry-pick", top_commit],
128+
check=False,
129+
stdout=subprocess.DEVNULL,
130+
stderr=subprocess.DEVNULL,
131+
)
132+
commit_message_result = subprocess.run(
133+
["git", "log", "-1", "--pretty=%B"],
134+
stdout=subprocess.PIPE,
135+
stderr=subprocess.PIPE,
136+
check=False,
137+
text=True,
138+
)
139+
commit_message = commit_message_result.stdout.strip()
140+
new_message = reformat_commit_message(commit_message)
141+
subprocess.run(
142+
["git", "commit", "--amend", "-m", new_message],
143+
check=False,
144+
stdout=subprocess.PIPE,
145+
stderr=subprocess.PIPE,
146+
)
147+
else:
148+
all_successful = False
149+
return all_successful
150+
151+
152+
def checkout_branch(branch):
153+
return (
154+
subprocess.run(
155+
["git", "checkout", branch],
156+
stdout=subprocess.DEVNULL,
157+
stderr=subprocess.DEVNULL,
158+
check=False,
159+
).returncode
160+
== 0
161+
)
162+
163+
164+
def validate_environment(element_name, base_branch):
165+
validations = [
166+
(is_cmd("git"), "Unable to find git in PATH"),
167+
(is_cmd("auto_updater"), "Unable to find auto_updater in PATH"),
168+
(is_git_dir(), "Current directory is not a git repository"),
169+
(not is_dirty(), "The repository is dirty"),
170+
]
171+
for valid, msg in validations:
172+
if not valid:
173+
logging.error(msg)
174+
return False
175+
return True
176+
177+
178+
def cleanup(branches, base_branch, branch_regex):
179+
checkout_branch(base_branch)
180+
clean_branches = [branch for branch in branches if re.match(branch_regex, branch)]
181+
for branch in clean_branches:
182+
if not delete_branch(branch):
183+
logging.error(f"Failed to delete local branch: {branch}")
184+
return False
185+
return True
186+
187+
188+
def main():
189+
parser = argparse.ArgumentParser(
190+
description="Run auto_updater and merge changes in a single branch"
191+
)
192+
parser.add_argument(
193+
"--no-cleanup",
194+
action="store_true",
195+
help="Do not delete auto_updater local branches",
196+
)
197+
parser.add_argument(
198+
"--base-branch",
199+
type=str,
200+
required=True,
201+
help="Specify the base branch",
202+
)
203+
parser.add_argument(
204+
"--element",
205+
type=str,
206+
required=True,
207+
help="Specify the element auto_updater will track",
208+
)
209+
args = parser.parse_args()
210+
211+
branch_regex = rf"^update/(components|include|abi|bootstrap|extensions)_.*[.](bst|yml)-diff_md5-.*-for-({args.base_branch})$"
212+
213+
if not validate_environment(args.element, args.base_branch):
214+
return 1
215+
216+
branches = get_local_branches()
217+
if not branches:
218+
logging.error("No branches found")
219+
return 1
220+
221+
if not args.no_cleanup and not cleanup(branches, args.base_branch, branch_regex):
222+
return 1
223+
224+
if run_updater(args.base_branch, args.element):
225+
if not is_dirty():
226+
new_branch = create_branch(args.base_branch)
227+
if new_branch:
228+
new_branches = [
229+
branch for branch in branches if re.match(branch_regex, branch)
230+
]
231+
if not cherry_pick_top_commit(new_branches, new_branch):
232+
logging.error("Failed to cherry-pick commit")
233+
return 1
234+
else:
235+
logging.error("Failed to create new branch")
236+
return 1
237+
else:
238+
logging.error(
239+
"The repository is dirty after running auto_updater"
240+
if is_dirty()
241+
else "Failed to checkout new branch"
242+
)
243+
return 1
244+
else:
245+
logging.error("auto_updater failed")
246+
return 1
247+
248+
return 0
249+
250+
251+
if __name__ == "__main__":
252+
raise SystemExit(main())

0 commit comments

Comments
 (0)