diff options
-rwxr-xr-x | scripts/combo-layer | 187 |
1 files changed, 174 insertions, 13 deletions
diff --git a/scripts/combo-layer b/scripts/combo-layer index 8ed9be8f37..d11274e245 100755 --- a/scripts/combo-layer +++ b/scripts/combo-layer | |||
@@ -25,6 +25,7 @@ import os, sys | |||
25 | import optparse | 25 | import optparse |
26 | import logging | 26 | import logging |
27 | import subprocess | 27 | import subprocess |
28 | import tempfile | ||
28 | import ConfigParser | 29 | import ConfigParser |
29 | import re | 30 | import re |
30 | from collections import OrderedDict | 31 | from collections import OrderedDict |
@@ -190,6 +191,11 @@ def action_init(conf, args): | |||
190 | subprocess.check_call("git clone %s %s" % (conf.repos[name]['src_uri'], ldir), shell=True) | 191 | subprocess.check_call("git clone %s %s" % (conf.repos[name]['src_uri'], ldir), shell=True) |
191 | if not os.path.exists(".git"): | 192 | if not os.path.exists(".git"): |
192 | runcmd("git init") | 193 | runcmd("git init") |
194 | if conf.history: | ||
195 | # Need a common ref for all trees. | ||
196 | runcmd('git commit -m "initial empty commit" --allow-empty') | ||
197 | startrev = runcmd('git rev-parse master').strip() | ||
198 | |||
193 | for name in conf.repos: | 199 | for name in conf.repos: |
194 | repo = conf.repos[name] | 200 | repo = conf.repos[name] |
195 | ldir = repo['local_repo_dir'] | 201 | ldir = repo['local_repo_dir'] |
@@ -205,6 +211,25 @@ def action_init(conf, args): | |||
205 | lastrev = None | 211 | lastrev = None |
206 | initialrev = branch | 212 | initialrev = branch |
207 | logger.info("Copying data from %s..." % name) | 213 | logger.info("Copying data from %s..." % name) |
214 | # Sanity check initialrev and turn it into hash (required for copying history, | ||
215 | # because resolving a name ref only works in the component repo). | ||
216 | rev = runcmd('git rev-parse %s' % initialrev, ldir).strip() | ||
217 | if rev != initialrev: | ||
218 | try: | ||
219 | refs = runcmd('git show-ref -s %s' % initialrev, ldir).split('\n') | ||
220 | if len(set(refs)) > 1: | ||
221 | # Happens for example when configured to track | ||
222 | # "master" and there is a refs/heads/master. The | ||
223 | # traditional behavior from "git archive" (preserved | ||
224 | # here) it to choose the first one. This might not be | ||
225 | # intended, so at least warn about it. | ||
226 | logger.warn("%s: initial revision '%s' not unique, picking result of rev-parse = %s" % | ||
227 | (name, initialrev, refs[0])) | ||
228 | initialrev = rev | ||
229 | except: | ||
230 | # show-ref fails for hashes. Skip the sanity warning in that case. | ||
231 | pass | ||
232 | initialrev = rev | ||
208 | dest_dir = repo['dest_dir'] | 233 | dest_dir = repo['dest_dir'] |
209 | if dest_dir and dest_dir != ".": | 234 | if dest_dir and dest_dir != ".": |
210 | extract_dir = os.path.join(os.getcwd(), dest_dir) | 235 | extract_dir = os.path.join(os.getcwd(), dest_dir) |
@@ -213,22 +238,155 @@ def action_init(conf, args): | |||
213 | else: | 238 | else: |
214 | extract_dir = os.getcwd() | 239 | extract_dir = os.getcwd() |
215 | file_filter = repo.get('file_filter', "") | 240 | file_filter = repo.get('file_filter', "") |
216 | files = runcmd("git archive %s | tar -x -v -C %s %s" % (initialrev, extract_dir, file_filter), ldir) | ||
217 | exclude_patterns = repo.get('file_exclude', '').split() | 241 | exclude_patterns = repo.get('file_exclude', '').split() |
218 | if exclude_patterns: | 242 | def copy_selected_files(initialrev, extract_dir, file_filter, exclude_patterns, ldir, |
219 | # Implement file removal by letting tar create the | 243 | subdir=""): |
220 | # file and then deleting it in the file system | 244 | # When working inside a filtered branch which had the |
221 | # again. Uses the list of files created by tar (easier | 245 | # files already moved, we need to prepend the |
222 | # than walking the tree). | 246 | # subdirectory to all filters, otherwise they would |
223 | for file in files.split('\n'): | 247 | # not match. |
224 | for pattern in exclude_patterns: | 248 | if subdir: |
225 | if fnmatch.fnmatch(file, pattern): | 249 | file_filter = ' '.join([subdir + '/' + x for x in file_filter.split()]) |
226 | os.unlink(os.path.join(extract_dir, file)) | 250 | exclude_patterns = [subdir + '/' + x for x in exclude_patterns] |
227 | break | 251 | # To handle both cases, we cd into the target |
252 | # directory and optionally tell tar to strip the path | ||
253 | # prefix when the files were already moved. | ||
254 | subdir_components = len(os.path.normpath(subdir).split(os.path.sep)) if subdir else 0 | ||
255 | strip=('--strip-components=%d' % subdir_components) if subdir else '' | ||
256 | # TODO: file_filter wild cards do not work (and haven't worked before either), because | ||
257 | # a) GNU tar requires a --wildcards parameter before turning on wild card matching. | ||
258 | # b) The semantic is not as intendend (src/*.c also matches src/foo/bar.c, | ||
259 | # in contrast to the other use of file_filter as parameter of "git archive" | ||
260 | # where it only matches .c files directly in src). | ||
261 | files = runcmd("git archive %s %s | tar -x -v %s -C %s %s" % | ||
262 | (initialrev, subdir, | ||
263 | strip, extract_dir, file_filter), | ||
264 | ldir) | ||
265 | if exclude_patterns: | ||
266 | # Implement file removal by letting tar create the | ||
267 | # file and then deleting it in the file system | ||
268 | # again. Uses the list of files created by tar (easier | ||
269 | # than walking the tree). | ||
270 | for file in files.split('\n'): | ||
271 | for pattern in exclude_patterns: | ||
272 | if fnmatch.fnmatch(file, pattern): | ||
273 | os.unlink(os.path.join(*([extract_dir] + ['..'] * subdir_components + [file]))) | ||
274 | break | ||
275 | |||
276 | if not conf.history: | ||
277 | copy_selected_files(initialrev, extract_dir, file_filter, exclude_patterns, ldir) | ||
278 | else: | ||
279 | # First fetch remote history into local repository. | ||
280 | # We need a ref for that, so ensure that there is one. | ||
281 | refname = "combo-layer-init-%s" % name | ||
282 | runcmd("git branch -f %s %s" % (refname, initialrev), ldir) | ||
283 | runcmd("git fetch %s %s" % (ldir, refname)) | ||
284 | runcmd("git branch -D %s" % refname, ldir) | ||
285 | # Make that the head revision. | ||
286 | runcmd("git checkout -b %s %s" % (name, initialrev)) | ||
287 | # Optional: rewrite history to change commit messages or to move files. | ||
288 | if 'hook' in repo or dest_dir and dest_dir != ".": | ||
289 | filter_branch = ['git', 'filter-branch', '--force'] | ||
290 | with tempfile.NamedTemporaryFile() as hookwrapper: | ||
291 | if 'hook' in repo: | ||
292 | # Create a shell script wrapper around the original hook that | ||
293 | # can be used by git filter-branch. Hook may or may not have | ||
294 | # an absolute path. | ||
295 | hook = repo['hook'] | ||
296 | hook = os.path.join(os.path.dirname(conf.conffile), '..', hook) | ||
297 | # The wrappers turns the commit message | ||
298 | # from stdin into a fake patch header. | ||
299 | # This is good enough for changing Subject | ||
300 | # and commit msg body with normal | ||
301 | # combo-layer hooks. | ||
302 | hookwrapper.write('''set -e | ||
303 | tmpname=$(mktemp) | ||
304 | trap "rm $tmpname" EXIT | ||
305 | echo -n 'Subject: [PATCH] ' >>$tmpname | ||
306 | cat >>$tmpname | ||
307 | if ! [ $(tail -c 1 $tmpname | od -A n -t x1) == '0a' ]; then | ||
308 | echo >>$tmpname | ||
309 | fi | ||
310 | echo '---' >>$tmpname | ||
311 | %s $tmpname $GIT_COMMIT %s | ||
312 | tail -c +18 $tmpname | head -c -4 | ||
313 | ''' % (hook, name)) | ||
314 | hookwrapper.flush() | ||
315 | filter_branch.extend(['--msg-filter', 'bash %s' % hookwrapper.name]) | ||
316 | if dest_dir and dest_dir != ".": | ||
317 | parent = os.path.dirname(dest_dir) | ||
318 | if not parent: | ||
319 | parent = '.' | ||
320 | # May run outside of the current directory, so do not assume that .git exists. | ||
321 | filter_branch.extend(['--tree-filter', 'mkdir -p .git/tmptree && mv $(ls -1 -a | grep -v -e ^.git$ -e ^.$ -e ^..$) .git/tmptree && mkdir -p %s && mv .git/tmptree %s' % (parent, dest_dir)]) | ||
322 | filter_branch.append('HEAD') | ||
323 | runcmd(filter_branch) | ||
324 | runcmd('git update-ref -d refs/original/refs/heads/%s' % name) | ||
325 | repo['rewritten_revision'] = runcmd('git rev-parse HEAD').strip() | ||
326 | repo['stripped_revision'] = repo['rewritten_revision'] | ||
327 | # Optional filter files: remove everything and re-populate using the normal filtering code. | ||
328 | # Override any potential .gitignore. | ||
329 | if file_filter or exclude_patterns: | ||
330 | runcmd('git rm -rf .') | ||
331 | if not os.path.exists(extract_dir): | ||
332 | os.makedirs(extract_dir) | ||
333 | copy_selected_files('HEAD', extract_dir, file_filter, exclude_patterns, '.', | ||
334 | subdir=dest_dir if dest_dir and dest_dir != '.' else '') | ||
335 | runcmd('git add --all --force .') | ||
336 | if runcmd('git status --porcelain'): | ||
337 | # Something to commit. | ||
338 | runcmd(['git', 'commit', '-m', | ||
339 | '''%s: select file subset | ||
340 | |||
341 | Files from the component repository were chosen based on | ||
342 | the following filters: | ||
343 | file_filter = %s | ||
344 | file_exclude = %s''' % (name, file_filter or '<empty>', repo.get('file_exclude', '<empty>'))]) | ||
345 | repo['stripped_revision'] = runcmd('git rev-parse HEAD').strip() | ||
346 | |||
228 | if not lastrev: | 347 | if not lastrev: |
229 | lastrev = runcmd("git rev-parse %s" % initialrev, ldir).strip() | 348 | lastrev = runcmd('git rev-parse %s' % initialrev, ldir).strip() |
230 | conf.update(name, "last_revision", lastrev, initmode=True) | 349 | conf.update(name, "last_revision", lastrev, initmode=True) |
231 | runcmd("git add .") | 350 | |
351 | if not conf.history: | ||
352 | runcmd("git add .") | ||
353 | else: | ||
354 | # Create Octopus merge commit according to http://stackoverflow.com/questions/10874149/git-octopus-merge-with-unrelated-repositoies | ||
355 | runcmd('git checkout master') | ||
356 | merge = ['git', 'merge', '--no-commit'] | ||
357 | with open('.git/info/grafts', 'w') as grafts: | ||
358 | grafts.write('%s\n' % startrev) | ||
359 | for name in conf.repos: | ||
360 | repo = conf.repos[name] | ||
361 | # Use branch created earlier. | ||
362 | merge.append(name) | ||
363 | for start in runcmd('git log --pretty=format:%%H --max-parents=0 %s' % name).split('\n'): | ||
364 | grafts.write('%s %s\n' % (start, startrev)) | ||
365 | try: | ||
366 | runcmd(merge) | ||
367 | except Exception, error: | ||
368 | logger.info('''Merging component repository history failed, perhaps because of merge conflicts. | ||
369 | It may be possible to commit anyway after resolving these conflicts. | ||
370 | |||
371 | %s''' % error) | ||
372 | # Create MERGE_HEAD and MERGE_MSG. "git merge" itself | ||
373 | # does not create MERGE_HEAD in case of a (harmless) failure, | ||
374 | # and we want certain auto-generated information in the | ||
375 | # commit message for future reference and/or automation. | ||
376 | with open('.git/MERGE_HEAD', 'w') as head: | ||
377 | with open('.git/MERGE_MSG', 'w') as msg: | ||
378 | msg.write('repo: initial import of components\n\n') | ||
379 | # head.write('%s\n' % startrev) | ||
380 | for name in conf.repos: | ||
381 | repo = conf.repos[name] | ||
382 | # <upstream ref> <rewritten ref> <rewritten + files removed> | ||
383 | msg.write('combo-layer-%s: %s %s %s\n' % (name, | ||
384 | repo['last_revision'], | ||
385 | repo['rewritten_revision'], | ||
386 | repo['stripped_revision'])) | ||
387 | rev = runcmd('git rev-parse %s' % name).strip() | ||
388 | head.write('%s\n' % rev) | ||
389 | |||
232 | if conf.localconffile: | 390 | if conf.localconffile: |
233 | localadded = True | 391 | localadded = True |
234 | try: | 392 | try: |
@@ -631,6 +789,9 @@ Action: | |||
631 | parser.add_option("-n", "--no-pull", help = "skip pulling component repos during update", | 789 | parser.add_option("-n", "--no-pull", help = "skip pulling component repos during update", |
632 | action = "store_true", dest = "nopull", default = False) | 790 | action = "store_true", dest = "nopull", default = False) |
633 | 791 | ||
792 | parser.add_option("-H", "--history", help = "import full history of components during init", | ||
793 | action = "store_true", default = False) | ||
794 | |||
634 | options, args = parser.parse_args(sys.argv) | 795 | options, args = parser.parse_args(sys.argv) |
635 | 796 | ||
636 | # Dispatch to action handler | 797 | # Dispatch to action handler |