combo-layer 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600
  1. #!/usr/bin/env python
  2. # ex:ts=4:sw=4:sts=4:et
  3. # -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
  4. #
  5. # Copyright 2011 Intel Corporation
  6. # Authored-by: Yu Ke <ke.yu@intel.com>
  7. # Paul Eggleton <paul.eggleton@intel.com>
  8. # Richard Purdie <richard.purdie@intel.com>
  9. #
  10. # This program is free software; you can redistribute it and/or modify
  11. # it under the terms of the GNU General Public License version 2 as
  12. # published by the Free Software Foundation.
  13. #
  14. # This program is distributed in the hope that it will be useful,
  15. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. # GNU General Public License for more details.
  18. #
  19. # You should have received a copy of the GNU General Public License along
  20. # with this program; if not, write to the Free Software Foundation, Inc.,
  21. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  22. import os, sys
  23. import optparse
  24. import logging
  25. import subprocess
  26. import ConfigParser
  27. import re
  28. __version__ = "0.2.1"
  29. def logger_create():
  30. logger = logging.getLogger("")
  31. loggerhandler = logging.StreamHandler()
  32. loggerhandler.setFormatter(logging.Formatter("[%(asctime)s] %(message)s","%H:%M:%S"))
  33. logger.addHandler(loggerhandler)
  34. logger.setLevel(logging.INFO)
  35. return logger
  36. logger = logger_create()
  37. def get_current_branch(repodir=None):
  38. try:
  39. if not os.path.exists(os.path.join(repodir if repodir else '', ".git")):
  40. # Repo not created yet (i.e. during init) so just assume master
  41. return "master"
  42. branchname = runcmd("git symbolic-ref HEAD 2>/dev/null", repodir).strip()
  43. if branchname.startswith("refs/heads/"):
  44. branchname = branchname[11:]
  45. return branchname
  46. except subprocess.CalledProcessError:
  47. return ""
  48. class Configuration(object):
  49. """
  50. Manages the configuration
  51. For an example config file, see combo-layer.conf.example
  52. """
  53. def __init__(self, options):
  54. for key, val in options.__dict__.items():
  55. setattr(self, key, val)
  56. def readsection(parser, section, repo):
  57. for (name, value) in parser.items(section):
  58. if value.startswith("@"):
  59. self.repos[repo][name] = eval(value.strip("@"))
  60. else:
  61. self.repos[repo][name] = value
  62. logger.debug("Loading config file %s" % self.conffile)
  63. self.parser = ConfigParser.ConfigParser()
  64. with open(self.conffile) as f:
  65. self.parser.readfp(f)
  66. self.repos = {}
  67. for repo in self.parser.sections():
  68. self.repos[repo] = {}
  69. readsection(self.parser, repo, repo)
  70. # Load local configuration, if available
  71. self.localconffile = None
  72. self.localparser = None
  73. self.combobranch = None
  74. if self.conffile.endswith('.conf'):
  75. lcfile = self.conffile.replace('.conf', '-local.conf')
  76. if os.path.exists(lcfile):
  77. # Read combo layer branch
  78. self.combobranch = get_current_branch()
  79. logger.debug("Combo layer branch is %s" % self.combobranch)
  80. self.localconffile = lcfile
  81. logger.debug("Loading local config file %s" % self.localconffile)
  82. self.localparser = ConfigParser.ConfigParser()
  83. with open(self.localconffile) as f:
  84. self.localparser.readfp(f)
  85. for section in self.localparser.sections():
  86. if '|' in section:
  87. sectionvals = section.split('|')
  88. repo = sectionvals[0]
  89. if sectionvals[1] != self.combobranch:
  90. continue
  91. else:
  92. repo = section
  93. if repo in self.repos:
  94. readsection(self.localparser, section, repo)
  95. def update(self, repo, option, value, initmode=False):
  96. if self.localparser:
  97. parser = self.localparser
  98. section = "%s|%s" % (repo, self.combobranch)
  99. conffile = self.localconffile
  100. if initmode and not parser.has_section(section):
  101. parser.add_section(section)
  102. else:
  103. parser = self.parser
  104. section = repo
  105. conffile = self.conffile
  106. parser.set(section, option, value)
  107. with open(conffile, "w") as f:
  108. parser.write(f)
  109. def sanity_check(self, initmode=False):
  110. required_options=["src_uri", "local_repo_dir", "dest_dir", "last_revision"]
  111. if initmode:
  112. required_options.remove("last_revision")
  113. msg = ""
  114. missing_options = []
  115. for name in self.repos:
  116. for option in required_options:
  117. if option not in self.repos[name]:
  118. msg = "%s\nOption %s is not defined for component %s" %(msg, option, name)
  119. missing_options.append(option)
  120. if msg != "":
  121. logger.error("configuration file %s has the following error: %s" % (self.conffile,msg))
  122. if self.localconffile and 'last_revision' in missing_options:
  123. logger.error("local configuration file %s may be missing configuration for combo branch %s" % (self.localconffile, self.combobranch))
  124. sys.exit(1)
  125. # filterdiff is required by action_splitpatch, so check its availability
  126. if subprocess.call("which filterdiff > /dev/null 2>&1", shell=True) != 0:
  127. logger.error("ERROR: patchutils package is missing, please install it (e.g. # apt-get install patchutils)")
  128. sys.exit(1)
  129. def runcmd(cmd,destdir=None,printerr=True):
  130. """
  131. execute command, raise CalledProcessError if fail
  132. return output if succeed
  133. """
  134. logger.debug("run cmd '%s' in %s" % (cmd, os.getcwd() if destdir is None else destdir))
  135. out = os.tmpfile()
  136. try:
  137. subprocess.check_call(cmd, stdout=out, stderr=out, cwd=destdir, shell=True)
  138. except subprocess.CalledProcessError,e:
  139. out.seek(0)
  140. if printerr:
  141. logger.error("%s" % out.read())
  142. raise e
  143. out.seek(0)
  144. output = out.read()
  145. logger.debug("output: %s" % output )
  146. return output
  147. def action_init(conf, args):
  148. """
  149. Clone component repositories
  150. Check git is initialised; if not, copy initial data from component repos
  151. """
  152. for name in conf.repos:
  153. ldir = conf.repos[name]['local_repo_dir']
  154. if not os.path.exists(ldir):
  155. logger.info("cloning %s to %s" %(conf.repos[name]['src_uri'], ldir))
  156. subprocess.check_call("git clone %s %s" % (conf.repos[name]['src_uri'], ldir), shell=True)
  157. if not os.path.exists(".git"):
  158. runcmd("git init")
  159. for name in conf.repos:
  160. repo = conf.repos[name]
  161. ldir = repo['local_repo_dir']
  162. branch = repo.get('branch', "master")
  163. lastrev = repo.get('last_revision', None)
  164. if lastrev and lastrev != "HEAD":
  165. initialrev = lastrev
  166. if branch:
  167. if not check_rev_branch(name, ldir, lastrev, branch):
  168. sys.exit(1)
  169. logger.info("Copying data from %s at specified revision %s..." % (name, lastrev))
  170. else:
  171. lastrev = None
  172. initialrev = branch
  173. logger.info("Copying data from %s..." % name)
  174. dest_dir = repo['dest_dir']
  175. if dest_dir and dest_dir != ".":
  176. extract_dir = os.path.join(os.getcwd(), dest_dir)
  177. if not os.path.exists(extract_dir):
  178. os.makedirs(extract_dir)
  179. else:
  180. extract_dir = os.getcwd()
  181. file_filter = repo.get('file_filter', "")
  182. runcmd("git archive %s | tar -x -C %s %s" % (initialrev, extract_dir, file_filter), ldir)
  183. if not lastrev:
  184. lastrev = runcmd("git rev-parse %s" % initialrev, ldir).strip()
  185. conf.update(name, "last_revision", lastrev, initmode=True)
  186. runcmd("git add .")
  187. if conf.localconffile:
  188. localadded = True
  189. try:
  190. runcmd("git rm --cached %s" % conf.localconffile, printerr=False)
  191. except subprocess.CalledProcessError:
  192. localadded = False
  193. if localadded:
  194. localrelpath = os.path.relpath(conf.localconffile)
  195. runcmd("grep -q %s .gitignore || echo %s >> .gitignore" % (localrelpath, localrelpath))
  196. runcmd("git add .gitignore")
  197. logger.info("Added local configuration file %s to .gitignore", localrelpath)
  198. logger.info("Initial combo layer repository data has been created; please make any changes if desired and then use 'git commit' to make the initial commit.")
  199. else:
  200. logger.info("Repository already initialised, nothing to do.")
  201. def check_repo_clean(repodir):
  202. """
  203. check if the repo is clean
  204. exit if repo is dirty
  205. """
  206. output=runcmd("git status --porcelain", repodir)
  207. r = re.compile('\?\? patch-.*/')
  208. dirtyout = [item for item in output.splitlines() if not r.match(item)]
  209. if dirtyout:
  210. logger.error("git repo %s is dirty, please fix it first", repodir)
  211. sys.exit(1)
  212. def check_patch(patchfile):
  213. f = open(patchfile)
  214. ln = f.readline()
  215. of = None
  216. in_patch = False
  217. beyond_msg = False
  218. pre_buf = ''
  219. while ln:
  220. if not beyond_msg:
  221. if ln == '---\n':
  222. if not of:
  223. break
  224. in_patch = False
  225. beyond_msg = True
  226. elif ln.startswith('--- '):
  227. # We have a diff in the commit message
  228. in_patch = True
  229. if not of:
  230. print('WARNING: %s contains a diff in its commit message, indenting to avoid failure during apply' % patchfile)
  231. of = open(patchfile + '.tmp', 'w')
  232. of.write(pre_buf)
  233. pre_buf = ''
  234. elif in_patch and not ln[0] in '+-@ \n\r':
  235. in_patch = False
  236. if of:
  237. if in_patch:
  238. of.write(' ' + ln)
  239. else:
  240. of.write(ln)
  241. else:
  242. pre_buf += ln
  243. ln = f.readline()
  244. f.close()
  245. if of:
  246. of.close()
  247. os.rename(patchfile + '.tmp', patchfile)
  248. def drop_to_shell(workdir=None):
  249. shell = os.environ.get('SHELL', 'bash')
  250. print('Dropping to shell "%s"\n' \
  251. 'When you are finished, run the following to continue:\n' \
  252. ' exit -- continue to apply the patches\n' \
  253. ' exit 1 -- abort\n' % shell);
  254. ret = subprocess.call([shell], cwd=workdir)
  255. if ret != 0:
  256. print "Aborting"
  257. return False
  258. else:
  259. return True
  260. def check_rev_branch(component, repodir, rev, branch):
  261. try:
  262. actualbranch = runcmd("git branch --contains %s" % rev, repodir, printerr=False)
  263. except subprocess.CalledProcessError as e:
  264. if e.returncode == 129:
  265. actualbranch = ""
  266. else:
  267. raise
  268. if not actualbranch:
  269. logger.error("%s: specified revision %s is invalid!" % (component, rev))
  270. return False
  271. branches = []
  272. branchlist = actualbranch.split("\n")
  273. for b in branchlist:
  274. branches.append(b.strip().split(' ')[-1])
  275. if branch not in branches:
  276. logger.error("%s: specified revision %s is not on specified branch %s!" % (component, rev, branch))
  277. return False
  278. return True
  279. def get_repos(conf, args):
  280. repos = []
  281. if len(args) > 1:
  282. for arg in args[1:]:
  283. if arg.startswith('-'):
  284. break
  285. else:
  286. repos.append(arg)
  287. for repo in repos:
  288. if not repo in conf.repos:
  289. logger.error("Specified component '%s' not found in configuration" % repo)
  290. sys.exit(0)
  291. if not repos:
  292. repos = conf.repos
  293. return repos
  294. def action_pull(conf, args):
  295. """
  296. update the component repos only
  297. """
  298. repos = get_repos(conf, args)
  299. # make sure all repos are clean
  300. for name in repos:
  301. check_repo_clean(conf.repos[name]['local_repo_dir'])
  302. for name in repos:
  303. repo = conf.repos[name]
  304. ldir = repo['local_repo_dir']
  305. branch = repo.get('branch', "master")
  306. runcmd("git checkout %s" % branch, ldir)
  307. logger.info("git pull for component repo %s in %s ..." % (name, ldir))
  308. output=runcmd("git pull", ldir)
  309. logger.info(output)
  310. def action_update(conf, args):
  311. """
  312. update the component repos
  313. generate the patch list
  314. apply the generated patches
  315. """
  316. repos = get_repos(conf, args)
  317. # make sure combo repo is clean
  318. check_repo_clean(os.getcwd())
  319. import uuid
  320. patch_dir = "patch-%s" % uuid.uuid4()
  321. if not os.path.exists(patch_dir):
  322. os.mkdir(patch_dir)
  323. # Step 1: update the component repos
  324. if conf.nopull:
  325. logger.info("Skipping pull (-n)")
  326. else:
  327. action_pull(conf, args)
  328. for name in repos:
  329. repo = conf.repos[name]
  330. ldir = repo['local_repo_dir']
  331. dest_dir = repo['dest_dir']
  332. branch = repo.get('branch', "master")
  333. repo_patch_dir = os.path.join(os.getcwd(), patch_dir, name)
  334. # Step 2: generate the patch list and store to patch dir
  335. logger.info("Generating patches from %s..." % name)
  336. if dest_dir != ".":
  337. prefix = "--src-prefix=a/%s/ --dst-prefix=b/%s/" % (dest_dir, dest_dir)
  338. else:
  339. prefix = ""
  340. if repo['last_revision'] == "":
  341. logger.info("Warning: last_revision of component %s is not set, starting from the first commit" % name)
  342. patch_cmd_range = "--root %s" % branch
  343. rev_cmd_range = branch
  344. else:
  345. if not check_rev_branch(name, ldir, repo['last_revision'], branch):
  346. sys.exit(1)
  347. patch_cmd_range = "%s..%s" % (repo['last_revision'], branch)
  348. rev_cmd_range = patch_cmd_range
  349. file_filter = repo.get('file_filter',"")
  350. patch_cmd = "git format-patch -N %s --output-directory %s %s -- %s" % \
  351. (prefix,repo_patch_dir, patch_cmd_range, file_filter)
  352. output = runcmd(patch_cmd, ldir)
  353. logger.debug("generated patch set:\n%s" % output)
  354. patchlist = output.splitlines()
  355. rev_cmd = "git rev-list --no-merges %s -- %s" % (rev_cmd_range, file_filter)
  356. revlist = runcmd(rev_cmd, ldir).splitlines()
  357. # Step 3: Call repo specific hook to adjust patch
  358. if 'hook' in repo:
  359. # hook parameter is: ./hook patchpath revision reponame
  360. count=len(revlist)-1
  361. for patch in patchlist:
  362. runcmd("%s %s %s %s" % (repo['hook'], patch, revlist[count], name))
  363. count=count-1
  364. # Step 4: write patch list and revision list to file, for user to edit later
  365. patchlist_file = os.path.join(os.getcwd(), patch_dir, "patchlist-%s" % name)
  366. repo['patchlist'] = patchlist_file
  367. f = open(patchlist_file, 'w')
  368. count=len(revlist)-1
  369. for patch in patchlist:
  370. f.write("%s %s\n" % (patch, revlist[count]))
  371. check_patch(os.path.join(patch_dir, patch))
  372. count=count-1
  373. f.close()
  374. # Step 5: invoke bash for user to edit patch and patch list
  375. if conf.interactive:
  376. print('You may now edit the patch and patch list in %s\n' \
  377. 'For example, you can remove unwanted patch entries from patchlist-*, so that they will be not applied later' % patch_dir);
  378. if not drop_to_shell(patch_dir):
  379. sys.exit(0)
  380. # Step 6: apply the generated and revised patch
  381. apply_patchlist(conf, repos)
  382. runcmd("rm -rf %s" % patch_dir)
  383. # Step 7: commit the updated config file if it's being tracked
  384. relpath = os.path.relpath(conf.conffile)
  385. try:
  386. output = runcmd("git status --porcelain %s" % relpath, printerr=False)
  387. except:
  388. # Outside the repository
  389. output = None
  390. if output:
  391. logger.info("Committing updated configuration file")
  392. if output.lstrip().startswith("M"):
  393. runcmd('git commit -m "Automatic commit to update last_revision" %s' % relpath)
  394. def apply_patchlist(conf, repos):
  395. """
  396. apply the generated patch list to combo repo
  397. """
  398. for name in repos:
  399. repo = conf.repos[name]
  400. lastrev = repo["last_revision"]
  401. prevrev = lastrev
  402. # Get non-blank lines from patch list file
  403. patchlist = []
  404. if os.path.exists(repo['patchlist']) or not conf.interactive:
  405. # Note: we want this to fail here if the file doesn't exist and we're not in
  406. # interactive mode since the file should exist in this case
  407. with open(repo['patchlist']) as f:
  408. for line in f:
  409. line = line.rstrip()
  410. if line:
  411. patchlist.append(line)
  412. if patchlist:
  413. logger.info("Applying patches from %s..." % name)
  414. linecount = len(patchlist)
  415. i = 1
  416. for line in patchlist:
  417. patchfile = line.split()[0]
  418. lastrev = line.split()[1]
  419. patchdisp = os.path.relpath(patchfile)
  420. if os.path.getsize(patchfile) == 0:
  421. logger.info("(skipping %d/%d %s - no changes)" % (i, linecount, patchdisp))
  422. else:
  423. cmd = "git am --keep-cr -s -p1 %s" % patchfile
  424. logger.info("Applying %d/%d: %s" % (i, linecount, patchdisp))
  425. try:
  426. runcmd(cmd)
  427. except subprocess.CalledProcessError:
  428. logger.info('Running "git am --abort" to cleanup repo')
  429. runcmd("git am --abort")
  430. logger.error('"%s" failed' % cmd)
  431. logger.info("Please manually apply patch %s" % patchdisp)
  432. logger.info("Note: if you exit and continue applying without manually applying the patch, it will be skipped")
  433. if not drop_to_shell():
  434. if prevrev != repo['last_revision']:
  435. conf.update(name, "last_revision", prevrev)
  436. sys.exit(0)
  437. prevrev = lastrev
  438. i += 1
  439. else:
  440. logger.info("No patches to apply from %s" % name)
  441. ldir = conf.repos[name]['local_repo_dir']
  442. branch = conf.repos[name].get('branch', "master")
  443. lastrev = runcmd("git rev-parse %s" % branch, ldir).strip()
  444. if lastrev != repo['last_revision']:
  445. conf.update(name, "last_revision", lastrev)
  446. def action_splitpatch(conf, args):
  447. """
  448. generate the commit patch and
  449. split the patch per repo
  450. """
  451. logger.debug("action_splitpatch")
  452. if len(args) > 1:
  453. commit = args[1]
  454. else:
  455. commit = "HEAD"
  456. patchdir = "splitpatch-%s" % commit
  457. if not os.path.exists(patchdir):
  458. os.mkdir(patchdir)
  459. # filerange_root is for the repo whose dest_dir is root "."
  460. # and it should be specified by excluding all other repo dest dir
  461. # like "-x repo1 -x repo2 -x repo3 ..."
  462. filerange_root = ""
  463. for name in conf.repos:
  464. dest_dir = conf.repos[name]['dest_dir']
  465. if dest_dir != ".":
  466. filerange_root = '%s -x "%s/*"' % (filerange_root, dest_dir)
  467. for name in conf.repos:
  468. dest_dir = conf.repos[name]['dest_dir']
  469. patch_filename = "%s/%s.patch" % (patchdir, name)
  470. if dest_dir == ".":
  471. cmd = "git format-patch -n1 --stdout %s^..%s | filterdiff -p1 %s > %s" % (commit, commit, filerange_root, patch_filename)
  472. else:
  473. cmd = "git format-patch --no-prefix -n1 --stdout %s^..%s -- %s > %s" % (commit, commit, dest_dir, patch_filename)
  474. runcmd(cmd)
  475. # Detect empty patches (including those produced by filterdiff above
  476. # that contain only preamble text)
  477. if os.path.getsize(patch_filename) == 0 or runcmd("filterdiff %s" % patch_filename) == "":
  478. os.remove(patch_filename)
  479. logger.info("(skipping %s - no changes)", name)
  480. else:
  481. logger.info(patch_filename)
  482. def action_error(conf, args):
  483. logger.info("invalid action %s" % args[0])
  484. actions = {
  485. "init": action_init,
  486. "update": action_update,
  487. "pull": action_pull,
  488. "splitpatch": action_splitpatch,
  489. }
  490. def main():
  491. parser = optparse.OptionParser(
  492. version = "Combo Layer Repo Tool version %s" % __version__,
  493. usage = """%prog [options] action
  494. Create and update a combination layer repository from multiple component repositories.
  495. Action:
  496. init initialise the combo layer repo
  497. update [components] get patches from component repos and apply them to the combo repo
  498. pull [components] just pull component repos only
  499. splitpatch [commit] generate commit patch and split per component, default commit is HEAD""")
  500. parser.add_option("-c", "--conf", help = "specify the config file (conf/combo-layer.conf is the default).",
  501. action = "store", dest = "conffile", default = "conf/combo-layer.conf")
  502. parser.add_option("-i", "--interactive", help = "interactive mode, user can edit the patch list and patches",
  503. action = "store_true", dest = "interactive", default = False)
  504. parser.add_option("-D", "--debug", help = "output debug information",
  505. action = "store_true", dest = "debug", default = False)
  506. parser.add_option("-n", "--no-pull", help = "skip pulling component repos during update",
  507. action = "store_true", dest = "nopull", default = False)
  508. options, args = parser.parse_args(sys.argv)
  509. # Dispatch to action handler
  510. if len(args) == 1:
  511. logger.error("No action specified, exiting")
  512. parser.print_help()
  513. elif args[1] not in actions:
  514. logger.error("Unsupported action %s, exiting\n" % (args[1]))
  515. parser.print_help()
  516. elif not os.path.exists(options.conffile):
  517. logger.error("No valid config file, exiting\n")
  518. parser.print_help()
  519. else:
  520. if options.debug:
  521. logger.setLevel(logging.DEBUG)
  522. confdata = Configuration(options)
  523. initmode = (args[1] == 'init')
  524. confdata.sanity_check(initmode)
  525. actions.get(args[1], action_error)(confdata, args[1:])
  526. if __name__ == "__main__":
  527. try:
  528. ret = main()
  529. except Exception:
  530. ret = 1
  531. import traceback
  532. traceback.print_exc(5)
  533. sys.exit(ret)