Skip to content
This repository was archived by the owner on Aug 31, 2021. It is now read-only.

Commit dee44b0

Browse files
committed
Add utility for updating submodule pointers.
Add a new tool that loops over all "develop" or branches in a remote repository, and update their submodule pointers. It won't do anything unless the submodules have been configured with appropriate "branch" keys in the `.gitmodules` file for that particular branch. At the moment, only the "develop" branch has the `.gitmodules` file configured appropriately. N.b. you *must* run this on a *completely clean* repository because it *nukes everything* (it'll warn you if there's any chance that you'll lose work).
1 parent ecc2b46 commit dee44b0

File tree

1 file changed

+255
-0
lines changed

1 file changed

+255
-0
lines changed

tools/update_submodules.py

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
#!/usr/bin/env python
2+
# Copyright (C) 2015 LiveCode Ltd.
3+
#
4+
# This file is part of LiveCode.
5+
#
6+
# LiveCode is free software; you can redistribute it and/or modify it under
7+
# the terms of the GNU General Public License v3 as published by the Free
8+
# Software Foundation.
9+
#
10+
# LiveCode is distributed in the hope that it will be useful, but WITHOUT ANY
11+
# WARRANTY; without even the implied warranty of MERCHANTABILITY or
12+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
13+
# for more details.
14+
#
15+
# You should have received a copy of the GNU General Public License
16+
# along with LiveCode. If not see <http://www.gnu.org/licenses/>.
17+
18+
import subprocess
19+
import re
20+
import sys
21+
import os.path
22+
import argparse
23+
24+
GIT='git'
25+
BRANCH_RE='^develop.*'
26+
TMP_BRANCH_FORMAT='submodule-tmp/{remote}/{branch}'
27+
28+
verbosity = 1
29+
dry_run = True
30+
31+
################################################################
32+
# Helper functions
33+
################################################################
34+
35+
def msg_debug(message):
36+
if verbosity >= 1:
37+
print('debug: {}'.format(message))
38+
39+
def msg_normal(message):
40+
if verbosity > 0:
41+
print(message)
42+
43+
def git_cmd(path, cmd):
44+
args = [GIT] + cmd
45+
msg_debug(' '.join(args))
46+
return subprocess.check_output(args, cwd=path)
47+
48+
def create_commit(path):
49+
message = 'Auto-update submodule pointers'
50+
cmd = ['commit', '--all', '--message', message]
51+
git_cmd(path, cmd)
52+
53+
def push_branch(path, remote, src, dest):
54+
cmd = ['push', remote, '{}:{}'.format(src,dest)]
55+
git_cmd(path, cmd)
56+
57+
################################################################
58+
# Get info about the local/remote repositories
59+
################################################################
60+
61+
def is_repo_clean(path):
62+
cmd = ['status', '--porcelain']
63+
output = git_cmd(path, cmd)
64+
return len(output.splitlines()) == 0
65+
66+
def get_branches(path, remote):
67+
cmd = ['branch', '--list', '--remotes', '{}/*'.format(remote)]
68+
output = git_cmd(path, cmd)
69+
70+
prefix = '{}/'.format(remote)
71+
def process_branch(branch):
72+
return branch.split()[0].replace(prefix,'')
73+
74+
return [process_branch(x) for x in output.splitlines()]
75+
76+
def get_candidate_branches(path, remote):
77+
expr = re.compile(BRANCH_RE)
78+
return [x
79+
for x in get_branches(path, remote)
80+
if expr.match(x) is not None]
81+
82+
################################################################
83+
# Submodule manipulation
84+
################################################################
85+
86+
def init_submodules(path):
87+
cmd = ['submodule', 'update', '--init']
88+
git_cmd(path, cmd)
89+
90+
# Clean up stale submodules. This is needed to cope with the fact
91+
# that prebuilts is a submodule in some branches but not in others.
92+
# Stale submodules are detected when a submodule exists in .git/config
93+
# but not in .submodules
94+
def clean_submodules(path):
95+
init_submodules(path)
96+
97+
cmd = ['config', '--local', '--list']
98+
mainconfig = git_cmd(path, cmd)
99+
100+
cmd = ['config', '--file', '.gitmodules', '--list']
101+
moduleconfig = git_cmd(path, cmd)
102+
103+
expr = re.compile('^submodule\.([^\.]*)\.')
104+
105+
current_submodules = []
106+
for config in moduleconfig.splitlines():
107+
match = expr.match(config)
108+
if match is None:
109+
continue
110+
if match.group(1) in current_submodules:
111+
continue
112+
current_submodules.append(match.group(1))
113+
114+
known_submodules = []
115+
for config in mainconfig.splitlines():
116+
match = expr.match(config)
117+
if match is None:
118+
continue
119+
if match.group(1) in known_submodules:
120+
continue
121+
known_submodules.append(match.group(1))
122+
123+
cleaned = False
124+
for mod in known_submodules:
125+
if mod in current_submodules:
126+
continue
127+
128+
# Okay, the module is (probably) stale, so blow it away
129+
msg_debug('removing {}'.format(mod))
130+
subprocess.check_call(['rm', '-rf', mod], cwd=path)
131+
132+
cleaned = True
133+
134+
# If anything was deleted, reset the tree
135+
if cleaned:
136+
cmd = ['reset', '--hard']
137+
git_cmd(path, cmd)
138+
139+
def sync_submodules(path):
140+
# Use the .gitmodules file to get a list of submodules that
141+
# can actually be synced
142+
cmd = ['config', '--file', '.gitmodules', '--list']
143+
moduleconfig = git_cmd(path, cmd)
144+
145+
expr = re.compile('^submodule\.([^\.]*)\.branch')
146+
for config in moduleconfig.splitlines():
147+
match = expr.match(config)
148+
if match is None:
149+
continue
150+
151+
# Modules which have remote branch information get
152+
# synchronised
153+
submodule = match.group(1)
154+
cmd = ['submodule', 'update', '--remote', submodule]
155+
git_cmd(path, cmd)
156+
157+
################################################################
158+
# Branch manipulation
159+
################################################################
160+
161+
def checkout_working_branch(path, remote, branch):
162+
working_name = TMP_BRANCH_FORMAT.format(remote=remote, branch=branch)
163+
cmd = ['branch', '--force', working_name, '{}/{}'.format(remote, branch)]
164+
git_cmd(path, cmd)
165+
166+
cmd = ['checkout', '--force', working_name]
167+
git_cmd(path, cmd)
168+
169+
cmd = ['clean', '--force', '-x', '-d']
170+
git_cmd(path, cmd)
171+
172+
clean_submodules(path)
173+
174+
return working_name
175+
176+
################################################################
177+
# Top-level worker functions
178+
################################################################
179+
180+
def process_branch(path, remote, branch):
181+
# Checkout a new local branch to work in
182+
tmp_branch = checkout_working_branch(path, remote, branch)
183+
184+
# Update submodules to match the remote branches
185+
sync_submodules(path)
186+
187+
# If the working tree is clean, skip to the next branch
188+
if is_repo_clean(path):
189+
msg_normal('ok - {} # SKIP no changes'.format(branch))
190+
return
191+
192+
create_commit(path)
193+
194+
if not dry_run:
195+
push_branch(path, remote, tmp_branch, branch)
196+
msg_normal('ok - {}'.format(branch))
197+
else:
198+
msg_normal('ok - {} # SKIP dry-run mode'.format(branch))
199+
200+
def process_repository(path, remote, branches=None):
201+
# Make sure all submodules have been initialised and checked out
202+
init_submodules(path)
203+
204+
# Fetch from the remote
205+
cmd = ['fetch', remote]
206+
git_cmd(path, cmd)
207+
208+
# Process each branch
209+
if branches is None:
210+
branches = get_candidate_branches(path, remote)
211+
212+
for branch in branches:
213+
process_branch(path, remote, branch)
214+
215+
################################################################
216+
# Main program
217+
################################################################
218+
219+
if __name__ == '__main__':
220+
parser = argparse.ArgumentParser(
221+
description='Automatically update submodule pointers.',
222+
epilog="""
223+
Automatically update all submodule pointers in the branches of the
224+
specified REMOTE repository. With `-n', doesn't push any changes to
225+
the remote repository. Use `-v' to get debugging info about what the
226+
tool is doing. If you don't specify a BRANCH, all `develop' branches
227+
will be updated.
228+
""")
229+
parser.add_argument('remote', help='name of remote repo to update',
230+
metavar='REMOTE', default='origin', nargs='?')
231+
parser.add_argument('branches', help='name of a remote branch to update',
232+
action='append', metavar='BRANCH', nargs='*')
233+
parser.add_argument('-n', help='disable pushing changes to remote repo',
234+
action='store_true', dest='dry_run')
235+
parser.add_argument('-C', metavar='PATH', help='run in a different directory',
236+
nargs=1, default='.', dest='repo')
237+
parser.add_argument('-v', help='increase debugging info',
238+
action='count', default=0, dest='verbosity')
239+
240+
args = parser.parse_args()
241+
242+
verbosity = args.verbosity
243+
dry_run = args.dry_run
244+
repo_path = args.repo
245+
remote = args.remote
246+
branches = args.branches
247+
248+
if not is_repo_clean(repo_path):
249+
print(
250+
"""Working tree is not clean.
251+
252+
You must run this tool in a clean repository""")
253+
sys.exit(1)
254+
255+
process_repository(repo_path, remote, branches)

0 commit comments

Comments
 (0)