PageRenderTime 69ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/hgext/transplant.py

https://bitbucket.org/mirror/mercurial/
Python | 689 lines | 654 code | 18 blank | 17 comment | 56 complexity | ad9f570dc219053ee54e87d3e9e47193 MD5 | raw file
Possible License(s): GPL-2.0
  1. # Patch transplanting extension for Mercurial
  2. #
  3. # Copyright 2006, 2007 Brendan Cully <brendan@kublai.com>
  4. #
  5. # This software may be used and distributed according to the terms of the
  6. # GNU General Public License version 2 or any later version.
  7. '''command to transplant changesets from another branch
  8. This extension allows you to transplant changes to another parent revision,
  9. possibly in another repository. The transplant is done using 'diff' patches.
  10. Transplanted patches are recorded in .hg/transplant/transplants, as a
  11. map from a changeset hash to its hash in the source repository.
  12. '''
  13. from mercurial.i18n import _
  14. import os, tempfile
  15. from mercurial.node import short
  16. from mercurial import bundlerepo, hg, merge, match
  17. from mercurial import patch, revlog, scmutil, util, error, cmdutil
  18. from mercurial import revset, templatekw
  19. class TransplantError(error.Abort):
  20. pass
  21. cmdtable = {}
  22. command = cmdutil.command(cmdtable)
  23. testedwith = 'internal'
  24. class transplantentry(object):
  25. def __init__(self, lnode, rnode):
  26. self.lnode = lnode
  27. self.rnode = rnode
  28. class transplants(object):
  29. def __init__(self, path=None, transplantfile=None, opener=None):
  30. self.path = path
  31. self.transplantfile = transplantfile
  32. self.opener = opener
  33. if not opener:
  34. self.opener = scmutil.opener(self.path)
  35. self.transplants = {}
  36. self.dirty = False
  37. self.read()
  38. def read(self):
  39. abspath = os.path.join(self.path, self.transplantfile)
  40. if self.transplantfile and os.path.exists(abspath):
  41. for line in self.opener.read(self.transplantfile).splitlines():
  42. lnode, rnode = map(revlog.bin, line.split(':'))
  43. list = self.transplants.setdefault(rnode, [])
  44. list.append(transplantentry(lnode, rnode))
  45. def write(self):
  46. if self.dirty and self.transplantfile:
  47. if not os.path.isdir(self.path):
  48. os.mkdir(self.path)
  49. fp = self.opener(self.transplantfile, 'w')
  50. for list in self.transplants.itervalues():
  51. for t in list:
  52. l, r = map(revlog.hex, (t.lnode, t.rnode))
  53. fp.write(l + ':' + r + '\n')
  54. fp.close()
  55. self.dirty = False
  56. def get(self, rnode):
  57. return self.transplants.get(rnode) or []
  58. def set(self, lnode, rnode):
  59. list = self.transplants.setdefault(rnode, [])
  60. list.append(transplantentry(lnode, rnode))
  61. self.dirty = True
  62. def remove(self, transplant):
  63. list = self.transplants.get(transplant.rnode)
  64. if list:
  65. del list[list.index(transplant)]
  66. self.dirty = True
  67. class transplanter(object):
  68. def __init__(self, ui, repo, opts):
  69. self.ui = ui
  70. self.path = repo.join('transplant')
  71. self.opener = scmutil.opener(self.path)
  72. self.transplants = transplants(self.path, 'transplants',
  73. opener=self.opener)
  74. self.editor = cmdutil.getcommiteditor(**opts)
  75. def applied(self, repo, node, parent):
  76. '''returns True if a node is already an ancestor of parent
  77. or is parent or has already been transplanted'''
  78. if hasnode(repo, parent):
  79. parentrev = repo.changelog.rev(parent)
  80. if hasnode(repo, node):
  81. rev = repo.changelog.rev(node)
  82. reachable = repo.changelog.ancestors([parentrev], rev,
  83. inclusive=True)
  84. if rev in reachable:
  85. return True
  86. for t in self.transplants.get(node):
  87. # it might have been stripped
  88. if not hasnode(repo, t.lnode):
  89. self.transplants.remove(t)
  90. return False
  91. lnoderev = repo.changelog.rev(t.lnode)
  92. if lnoderev in repo.changelog.ancestors([parentrev], lnoderev,
  93. inclusive=True):
  94. return True
  95. return False
  96. def apply(self, repo, source, revmap, merges, opts={}):
  97. '''apply the revisions in revmap one by one in revision order'''
  98. revs = sorted(revmap)
  99. p1, p2 = repo.dirstate.parents()
  100. pulls = []
  101. diffopts = patch.diffopts(self.ui, opts)
  102. diffopts.git = True
  103. lock = wlock = tr = None
  104. try:
  105. wlock = repo.wlock()
  106. lock = repo.lock()
  107. tr = repo.transaction('transplant')
  108. for rev in revs:
  109. node = revmap[rev]
  110. revstr = '%s:%s' % (rev, short(node))
  111. if self.applied(repo, node, p1):
  112. self.ui.warn(_('skipping already applied revision %s\n') %
  113. revstr)
  114. continue
  115. parents = source.changelog.parents(node)
  116. if not (opts.get('filter') or opts.get('log')):
  117. # If the changeset parent is the same as the
  118. # wdir's parent, just pull it.
  119. if parents[0] == p1:
  120. pulls.append(node)
  121. p1 = node
  122. continue
  123. if pulls:
  124. if source != repo:
  125. repo.pull(source.peer(), heads=pulls)
  126. merge.update(repo, pulls[-1], False, False, None)
  127. p1, p2 = repo.dirstate.parents()
  128. pulls = []
  129. domerge = False
  130. if node in merges:
  131. # pulling all the merge revs at once would mean we
  132. # couldn't transplant after the latest even if
  133. # transplants before them fail.
  134. domerge = True
  135. if not hasnode(repo, node):
  136. repo.pull(source.peer(), heads=[node])
  137. skipmerge = False
  138. if parents[1] != revlog.nullid:
  139. if not opts.get('parent'):
  140. self.ui.note(_('skipping merge changeset %s:%s\n')
  141. % (rev, short(node)))
  142. skipmerge = True
  143. else:
  144. parent = source.lookup(opts['parent'])
  145. if parent not in parents:
  146. raise util.Abort(_('%s is not a parent of %s') %
  147. (short(parent), short(node)))
  148. else:
  149. parent = parents[0]
  150. if skipmerge:
  151. patchfile = None
  152. else:
  153. fd, patchfile = tempfile.mkstemp(prefix='hg-transplant-')
  154. fp = os.fdopen(fd, 'w')
  155. gen = patch.diff(source, parent, node, opts=diffopts)
  156. for chunk in gen:
  157. fp.write(chunk)
  158. fp.close()
  159. del revmap[rev]
  160. if patchfile or domerge:
  161. try:
  162. try:
  163. n = self.applyone(repo, node,
  164. source.changelog.read(node),
  165. patchfile, merge=domerge,
  166. log=opts.get('log'),
  167. filter=opts.get('filter'))
  168. except TransplantError:
  169. # Do not rollback, it is up to the user to
  170. # fix the merge or cancel everything
  171. tr.close()
  172. raise
  173. if n and domerge:
  174. self.ui.status(_('%s merged at %s\n') % (revstr,
  175. short(n)))
  176. elif n:
  177. self.ui.status(_('%s transplanted to %s\n')
  178. % (short(node),
  179. short(n)))
  180. finally:
  181. if patchfile:
  182. os.unlink(patchfile)
  183. tr.close()
  184. if pulls:
  185. repo.pull(source.peer(), heads=pulls)
  186. merge.update(repo, pulls[-1], False, False, None)
  187. finally:
  188. self.saveseries(revmap, merges)
  189. self.transplants.write()
  190. if tr:
  191. tr.release()
  192. lock.release()
  193. wlock.release()
  194. def filter(self, filter, node, changelog, patchfile):
  195. '''arbitrarily rewrite changeset before applying it'''
  196. self.ui.status(_('filtering %s\n') % patchfile)
  197. user, date, msg = (changelog[1], changelog[2], changelog[4])
  198. fd, headerfile = tempfile.mkstemp(prefix='hg-transplant-')
  199. fp = os.fdopen(fd, 'w')
  200. fp.write("# HG changeset patch\n")
  201. fp.write("# User %s\n" % user)
  202. fp.write("# Date %d %d\n" % date)
  203. fp.write(msg + '\n')
  204. fp.close()
  205. try:
  206. util.system('%s %s %s' % (filter, util.shellquote(headerfile),
  207. util.shellquote(patchfile)),
  208. environ={'HGUSER': changelog[1],
  209. 'HGREVISION': revlog.hex(node),
  210. },
  211. onerr=util.Abort, errprefix=_('filter failed'),
  212. out=self.ui.fout)
  213. user, date, msg = self.parselog(file(headerfile))[1:4]
  214. finally:
  215. os.unlink(headerfile)
  216. return (user, date, msg)
  217. def applyone(self, repo, node, cl, patchfile, merge=False, log=False,
  218. filter=None):
  219. '''apply the patch in patchfile to the repository as a transplant'''
  220. (manifest, user, (time, timezone), files, message) = cl[:5]
  221. date = "%d %d" % (time, timezone)
  222. extra = {'transplant_source': node}
  223. if filter:
  224. (user, date, message) = self.filter(filter, node, cl, patchfile)
  225. if log:
  226. # we don't translate messages inserted into commits
  227. message += '\n(transplanted from %s)' % revlog.hex(node)
  228. self.ui.status(_('applying %s\n') % short(node))
  229. self.ui.note('%s %s\n%s\n' % (user, date, message))
  230. if not patchfile and not merge:
  231. raise util.Abort(_('can only omit patchfile if merging'))
  232. if patchfile:
  233. try:
  234. files = set()
  235. patch.patch(self.ui, repo, patchfile, files=files, eolmode=None)
  236. files = list(files)
  237. except Exception, inst:
  238. seriespath = os.path.join(self.path, 'series')
  239. if os.path.exists(seriespath):
  240. os.unlink(seriespath)
  241. p1 = repo.dirstate.p1()
  242. p2 = node
  243. self.log(user, date, message, p1, p2, merge=merge)
  244. self.ui.write(str(inst) + '\n')
  245. raise TransplantError(_('fix up the merge and run '
  246. 'hg transplant --continue'))
  247. else:
  248. files = None
  249. if merge:
  250. p1, p2 = repo.dirstate.parents()
  251. repo.setparents(p1, node)
  252. m = match.always(repo.root, '')
  253. else:
  254. m = match.exact(repo.root, '', files)
  255. n = repo.commit(message, user, date, extra=extra, match=m,
  256. editor=self.editor)
  257. if not n:
  258. self.ui.warn(_('skipping emptied changeset %s\n') % short(node))
  259. return None
  260. if not merge:
  261. self.transplants.set(n, node)
  262. return n
  263. def resume(self, repo, source, opts):
  264. '''recover last transaction and apply remaining changesets'''
  265. if os.path.exists(os.path.join(self.path, 'journal')):
  266. n, node = self.recover(repo, source, opts)
  267. self.ui.status(_('%s transplanted as %s\n') % (short(node),
  268. short(n)))
  269. seriespath = os.path.join(self.path, 'series')
  270. if not os.path.exists(seriespath):
  271. self.transplants.write()
  272. return
  273. nodes, merges = self.readseries()
  274. revmap = {}
  275. for n in nodes:
  276. revmap[source.changelog.rev(n)] = n
  277. os.unlink(seriespath)
  278. self.apply(repo, source, revmap, merges, opts)
  279. def recover(self, repo, source, opts):
  280. '''commit working directory using journal metadata'''
  281. node, user, date, message, parents = self.readlog()
  282. merge = False
  283. if not user or not date or not message or not parents[0]:
  284. raise util.Abort(_('transplant log file is corrupt'))
  285. parent = parents[0]
  286. if len(parents) > 1:
  287. if opts.get('parent'):
  288. parent = source.lookup(opts['parent'])
  289. if parent not in parents:
  290. raise util.Abort(_('%s is not a parent of %s') %
  291. (short(parent), short(node)))
  292. else:
  293. merge = True
  294. extra = {'transplant_source': node}
  295. wlock = repo.wlock()
  296. try:
  297. p1, p2 = repo.dirstate.parents()
  298. if p1 != parent:
  299. raise util.Abort(
  300. _('working dir not at transplant parent %s') %
  301. revlog.hex(parent))
  302. if merge:
  303. repo.setparents(p1, parents[1])
  304. n = repo.commit(message, user, date, extra=extra,
  305. editor=self.editor)
  306. if not n:
  307. raise util.Abort(_('commit failed'))
  308. if not merge:
  309. self.transplants.set(n, node)
  310. self.unlog()
  311. return n, node
  312. finally:
  313. wlock.release()
  314. def readseries(self):
  315. nodes = []
  316. merges = []
  317. cur = nodes
  318. for line in self.opener.read('series').splitlines():
  319. if line.startswith('# Merges'):
  320. cur = merges
  321. continue
  322. cur.append(revlog.bin(line))
  323. return (nodes, merges)
  324. def saveseries(self, revmap, merges):
  325. if not revmap:
  326. return
  327. if not os.path.isdir(self.path):
  328. os.mkdir(self.path)
  329. series = self.opener('series', 'w')
  330. for rev in sorted(revmap):
  331. series.write(revlog.hex(revmap[rev]) + '\n')
  332. if merges:
  333. series.write('# Merges\n')
  334. for m in merges:
  335. series.write(revlog.hex(m) + '\n')
  336. series.close()
  337. def parselog(self, fp):
  338. parents = []
  339. message = []
  340. node = revlog.nullid
  341. inmsg = False
  342. user = None
  343. date = None
  344. for line in fp.read().splitlines():
  345. if inmsg:
  346. message.append(line)
  347. elif line.startswith('# User '):
  348. user = line[7:]
  349. elif line.startswith('# Date '):
  350. date = line[7:]
  351. elif line.startswith('# Node ID '):
  352. node = revlog.bin(line[10:])
  353. elif line.startswith('# Parent '):
  354. parents.append(revlog.bin(line[9:]))
  355. elif not line.startswith('# '):
  356. inmsg = True
  357. message.append(line)
  358. if None in (user, date):
  359. raise util.Abort(_("filter corrupted changeset (no user or date)"))
  360. return (node, user, date, '\n'.join(message), parents)
  361. def log(self, user, date, message, p1, p2, merge=False):
  362. '''journal changelog metadata for later recover'''
  363. if not os.path.isdir(self.path):
  364. os.mkdir(self.path)
  365. fp = self.opener('journal', 'w')
  366. fp.write('# User %s\n' % user)
  367. fp.write('# Date %s\n' % date)
  368. fp.write('# Node ID %s\n' % revlog.hex(p2))
  369. fp.write('# Parent ' + revlog.hex(p1) + '\n')
  370. if merge:
  371. fp.write('# Parent ' + revlog.hex(p2) + '\n')
  372. fp.write(message.rstrip() + '\n')
  373. fp.close()
  374. def readlog(self):
  375. return self.parselog(self.opener('journal'))
  376. def unlog(self):
  377. '''remove changelog journal'''
  378. absdst = os.path.join(self.path, 'journal')
  379. if os.path.exists(absdst):
  380. os.unlink(absdst)
  381. def transplantfilter(self, repo, source, root):
  382. def matchfn(node):
  383. if self.applied(repo, node, root):
  384. return False
  385. if source.changelog.parents(node)[1] != revlog.nullid:
  386. return False
  387. extra = source.changelog.read(node)[5]
  388. cnode = extra.get('transplant_source')
  389. if cnode and self.applied(repo, cnode, root):
  390. return False
  391. return True
  392. return matchfn
  393. def hasnode(repo, node):
  394. try:
  395. return repo.changelog.rev(node) is not None
  396. except error.RevlogError:
  397. return False
  398. def browserevs(ui, repo, nodes, opts):
  399. '''interactively transplant changesets'''
  400. displayer = cmdutil.show_changeset(ui, repo, opts)
  401. transplants = []
  402. merges = []
  403. prompt = _('apply changeset? [ynmpcq?]:'
  404. '$$ &yes, transplant this changeset'
  405. '$$ &no, skip this changeset'
  406. '$$ &merge at this changeset'
  407. '$$ show &patch'
  408. '$$ &commit selected changesets'
  409. '$$ &quit and cancel transplant'
  410. '$$ &? (show this help)')
  411. for node in nodes:
  412. displayer.show(repo[node])
  413. action = None
  414. while not action:
  415. action = 'ynmpcq?'[ui.promptchoice(prompt)]
  416. if action == '?':
  417. for c, t in ui.extractchoices(prompt)[1]:
  418. ui.write('%s: %s\n' % (c, t))
  419. action = None
  420. elif action == 'p':
  421. parent = repo.changelog.parents(node)[0]
  422. for chunk in patch.diff(repo, parent, node):
  423. ui.write(chunk)
  424. action = None
  425. if action == 'y':
  426. transplants.append(node)
  427. elif action == 'm':
  428. merges.append(node)
  429. elif action == 'c':
  430. break
  431. elif action == 'q':
  432. transplants = ()
  433. merges = ()
  434. break
  435. displayer.close()
  436. return (transplants, merges)
  437. @command('transplant',
  438. [('s', 'source', '', _('transplant changesets from REPO'), _('REPO')),
  439. ('b', 'branch', [], _('use this source changeset as head'), _('REV')),
  440. ('a', 'all', None, _('pull all changesets up to the --branch revisions')),
  441. ('p', 'prune', [], _('skip over REV'), _('REV')),
  442. ('m', 'merge', [], _('merge at REV'), _('REV')),
  443. ('', 'parent', '',
  444. _('parent to choose when transplanting merge'), _('REV')),
  445. ('e', 'edit', False, _('invoke editor on commit messages')),
  446. ('', 'log', None, _('append transplant info to log message')),
  447. ('c', 'continue', None, _('continue last transplant session '
  448. 'after fixing conflicts')),
  449. ('', 'filter', '',
  450. _('filter changesets through command'), _('CMD'))],
  451. _('hg transplant [-s REPO] [-b BRANCH [-a]] [-p REV] '
  452. '[-m REV] [REV]...'))
  453. def transplant(ui, repo, *revs, **opts):
  454. '''transplant changesets from another branch
  455. Selected changesets will be applied on top of the current working
  456. directory with the log of the original changeset. The changesets
  457. are copied and will thus appear twice in the history with different
  458. identities.
  459. Consider using the graft command if everything is inside the same
  460. repository - it will use merges and will usually give a better result.
  461. Use the rebase extension if the changesets are unpublished and you want
  462. to move them instead of copying them.
  463. If --log is specified, log messages will have a comment appended
  464. of the form::
  465. (transplanted from CHANGESETHASH)
  466. You can rewrite the changelog message with the --filter option.
  467. Its argument will be invoked with the current changelog message as
  468. $1 and the patch as $2.
  469. --source/-s specifies another repository to use for selecting changesets,
  470. just as if it temporarily had been pulled.
  471. If --branch/-b is specified, these revisions will be used as
  472. heads when deciding which changesets to transplant, just as if only
  473. these revisions had been pulled.
  474. If --all/-a is specified, all the revisions up to the heads specified
  475. with --branch will be transplanted.
  476. Example:
  477. - transplant all changes up to REV on top of your current revision::
  478. hg transplant --branch REV --all
  479. You can optionally mark selected transplanted changesets as merge
  480. changesets. You will not be prompted to transplant any ancestors
  481. of a merged transplant, and you can merge descendants of them
  482. normally instead of transplanting them.
  483. Merge changesets may be transplanted directly by specifying the
  484. proper parent changeset by calling :hg:`transplant --parent`.
  485. If no merges or revisions are provided, :hg:`transplant` will
  486. start an interactive changeset browser.
  487. If a changeset application fails, you can fix the merge by hand
  488. and then resume where you left off by calling :hg:`transplant
  489. --continue/-c`.
  490. '''
  491. def incwalk(repo, csets, match=util.always):
  492. for node in csets:
  493. if match(node):
  494. yield node
  495. def transplantwalk(repo, dest, heads, match=util.always):
  496. '''Yield all nodes that are ancestors of a head but not ancestors
  497. of dest.
  498. If no heads are specified, the heads of repo will be used.'''
  499. if not heads:
  500. heads = repo.heads()
  501. ancestors = []
  502. ctx = repo[dest]
  503. for head in heads:
  504. ancestors.append(ctx.ancestor(repo[head]).node())
  505. for node in repo.changelog.nodesbetween(ancestors, heads)[0]:
  506. if match(node):
  507. yield node
  508. def checkopts(opts, revs):
  509. if opts.get('continue'):
  510. if opts.get('branch') or opts.get('all') or opts.get('merge'):
  511. raise util.Abort(_('--continue is incompatible with '
  512. '--branch, --all and --merge'))
  513. return
  514. if not (opts.get('source') or revs or
  515. opts.get('merge') or opts.get('branch')):
  516. raise util.Abort(_('no source URL, branch revision or revision '
  517. 'list provided'))
  518. if opts.get('all'):
  519. if not opts.get('branch'):
  520. raise util.Abort(_('--all requires a branch revision'))
  521. if revs:
  522. raise util.Abort(_('--all is incompatible with a '
  523. 'revision list'))
  524. checkopts(opts, revs)
  525. if not opts.get('log'):
  526. opts['log'] = ui.config('transplant', 'log')
  527. if not opts.get('filter'):
  528. opts['filter'] = ui.config('transplant', 'filter')
  529. tp = transplanter(ui, repo, opts)
  530. cmdutil.checkunfinished(repo)
  531. p1, p2 = repo.dirstate.parents()
  532. if len(repo) > 0 and p1 == revlog.nullid:
  533. raise util.Abort(_('no revision checked out'))
  534. if not opts.get('continue'):
  535. if p2 != revlog.nullid:
  536. raise util.Abort(_('outstanding uncommitted merges'))
  537. m, a, r, d = repo.status()[:4]
  538. if m or a or r or d:
  539. raise util.Abort(_('outstanding local changes'))
  540. sourcerepo = opts.get('source')
  541. if sourcerepo:
  542. peer = hg.peer(repo, opts, ui.expandpath(sourcerepo))
  543. heads = map(peer.lookup, opts.get('branch', ()))
  544. source, csets, cleanupfn = bundlerepo.getremotechanges(ui, repo, peer,
  545. onlyheads=heads, force=True)
  546. else:
  547. source = repo
  548. heads = map(source.lookup, opts.get('branch', ()))
  549. cleanupfn = None
  550. try:
  551. if opts.get('continue'):
  552. tp.resume(repo, source, opts)
  553. return
  554. tf = tp.transplantfilter(repo, source, p1)
  555. if opts.get('prune'):
  556. prune = set(source.lookup(r)
  557. for r in scmutil.revrange(source, opts.get('prune')))
  558. matchfn = lambda x: tf(x) and x not in prune
  559. else:
  560. matchfn = tf
  561. merges = map(source.lookup, opts.get('merge', ()))
  562. revmap = {}
  563. if revs:
  564. for r in scmutil.revrange(source, revs):
  565. revmap[int(r)] = source.lookup(r)
  566. elif opts.get('all') or not merges:
  567. if source != repo:
  568. alltransplants = incwalk(source, csets, match=matchfn)
  569. else:
  570. alltransplants = transplantwalk(source, p1, heads,
  571. match=matchfn)
  572. if opts.get('all'):
  573. revs = alltransplants
  574. else:
  575. revs, newmerges = browserevs(ui, source, alltransplants, opts)
  576. merges.extend(newmerges)
  577. for r in revs:
  578. revmap[source.changelog.rev(r)] = r
  579. for r in merges:
  580. revmap[source.changelog.rev(r)] = r
  581. tp.apply(repo, source, revmap, merges, opts)
  582. finally:
  583. if cleanupfn:
  584. cleanupfn()
  585. def revsettransplanted(repo, subset, x):
  586. """``transplanted([set])``
  587. Transplanted changesets in set, or all transplanted changesets.
  588. """
  589. if x:
  590. s = revset.getset(repo, subset, x)
  591. else:
  592. s = subset
  593. return revset.baseset([r for r in s if
  594. repo[r].extra().get('transplant_source')])
  595. def kwtransplanted(repo, ctx, **args):
  596. """:transplanted: String. The node identifier of the transplanted
  597. changeset if any."""
  598. n = ctx.extra().get('transplant_source')
  599. return n and revlog.hex(n) or ''
  600. def extsetup(ui):
  601. revset.symbols['transplanted'] = revsettransplanted
  602. templatekw.keywords['transplanted'] = kwtransplanted
  603. cmdutil.unfinishedstates.append(
  604. ['series', True, False, _('transplant in progress'),
  605. _("use 'hg transplant --continue' or 'hg update' to abort")])
  606. # tell hggettext to extract docstrings from these functions:
  607. i18nfunctions = [revsettransplanted, kwtransplanted]