diff --git a/bin/loman b/bin/loman
new file mode 120000
index 0000000000..d0f8b11e30
--- /dev/null
+++ b/bin/loman
@@ -0,0 +1 @@
+loman.py
\ No newline at end of file
diff --git a/bin/loman.py b/bin/loman.py
new file mode 100755
index 0000000000..7575e2904f
--- /dev/null
+++ b/bin/loman.py
@@ -0,0 +1,96 @@
+#!/usr/bin/python
+
+# Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""This module allows adding and deleting of projects to the local manifest."""
+
+import sys
+import optparse
+import os
+import xml.etree.ElementTree as ElementTree
+
+from cros_build_lib import Die
+
+
+def _FindRepoDir():
+ cwd = os.getcwd()
+ while cwd != '/':
+ repo_dir = os.path.join(cwd, '.repo')
+ if os.path.isdir(repo_dir):
+ return repo_dir
+ cwd = os.path.dirname(cwd)
+ return None
+
+
+class LocalManifest:
+ """Class which provides an abstraction for manipulating the local manifest."""
+
+ def __init__(self, text=None):
+ self._text = text or '\n'
+
+ def Parse(self):
+ """Parse the manifest."""
+ self._root = ElementTree.fromstring(self._text)
+
+ def AddWorkonProject(self, name, path):
+ """Add a new workon project if it is not already in the manifest.
+
+ Returns:
+ True on success.
+ """
+
+ for project in self._root.findall('project'):
+ if project.attrib['path'] == path or project.attrib['name'] == name:
+ return False
+ self._AddProject(name, path, workon='True')
+ return True
+
+ def _AddProject(self, name, path, workon='False'):
+ element = ElementTree.Element('project', name=name, path=path,
+ workon=workon)
+ element.tail = '\n'
+ self._root.append(element)
+
+ def ToString(self):
+ return ElementTree.tostring(self._root, encoding='UTF-8')
+
+
+def main(argv):
+ usage = 'usage: %prog add [options] '
+ parser = optparse.OptionParser(usage=usage)
+ parser.add_option('-w', '--workon', action='store_true', dest='workon',
+ default=False, help='Is this a workon package?')
+ parser.add_option('-f', '--file', dest='manifest',
+ help='Non-default manifest file to read.')
+ (options, args) = parser.parse_args(argv[2:])
+ if len(args) < 2:
+ parser.error('Not enough arguments')
+ if argv[1] not in ['add']:
+ parser.error('Unsupported command: %s.' % argv[1])
+ if not options.workon:
+ parser.error('Adding of non-workon projects is currently unsupported.')
+ (name, path) = (args[0], args[1])
+
+ repo_dir = _FindRepoDir()
+ if not repo_dir:
+ Die("Unable to find repo dir.")
+ local_manifest = options.manifest or \
+ os.path.join(_FindRepoDir(), 'local_manifest.xml')
+ if os.path.isfile(local_manifest):
+ ptree = LocalManifest(open(local_manifest).read())
+ else:
+ ptree = LocalManifest()
+ ptree.Parse()
+ if not ptree.AddWorkonProject(name, path):
+ Die('Path "%s" or name "%s" already exits in the manifest.' %
+ (path, name))
+ try:
+ print >> open(local_manifest, 'w'), ptree.ToString()
+ except Exception, e:
+ Die('Error writing to manifest: %s' % e)
+
+
+if __name__ == '__main__':
+ main(sys.argv)
diff --git a/bin/loman_unittest.py b/bin/loman_unittest.py
new file mode 100755
index 0000000000..fc74ac7af2
--- /dev/null
+++ b/bin/loman_unittest.py
@@ -0,0 +1,111 @@
+#!/usr/bin/python
+
+# Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Unittests for loman."""
+
+import os
+import StringIO
+import sys
+import tempfile
+import unittest
+
+import loman
+
+_TEST_MANIFEST1 = """
+
+"""
+
+class LocalManifestTest(unittest.TestCase):
+
+ def setUp(self):
+ self.utf8 = "\n"
+ self.tiny_manifest = '\n'
+
+ def testSimpleParse(self):
+ ptree = loman.LocalManifest()
+ ptree.Parse()
+
+ def testParse(self):
+ ptree = loman.LocalManifest(self.tiny_manifest)
+ ptree.Parse()
+ self.assertEqual(ptree.ToString(), self.utf8 + self.tiny_manifest)
+
+ def testUTF8Parse(self):
+ ptree = loman.LocalManifest(self.utf8 + self.tiny_manifest)
+ ptree.Parse()
+ self.assertEqual(ptree.ToString(), self.utf8 + self.tiny_manifest)
+
+ def testAddNew(self):
+ ptree = loman.LocalManifest('\n')
+ ptree.Parse()
+ self.assertTrue(ptree.AddWorkonProject('foo', 'path/to/foo'))
+ self.assertEqual(
+ ptree.ToString(),
+ self.utf8 + '\n'
+ '\n'
+ '')
+
+ def testAddDup(self):
+ ptree = loman.LocalManifest('\n')
+ ptree.Parse()
+ ptree.AddWorkonProject('foo', 'path/to/foo')
+ self.assertTrue(not ptree.AddWorkonProject('foo', 'path/to/foo'))
+ self.assertTrue(not ptree.AddWorkonProject('foo', 'path/foo'))
+ self.assertTrue(not ptree.AddWorkonProject('foobar', 'path/to/foo'))
+
+class MainTest(unittest.TestCase):
+
+ def setUp(self):
+ self.utf8 = "\n"
+ self.tiny_manifest = '\n'
+ self.stderr = sys.stderr
+ sys.stderr = StringIO.StringIO()
+
+ def tearDown(self):
+ sys.stderr = self.stderr
+
+ def testNotEnoughArgs(self):
+ err_msg = 'Not enough arguments\n'
+ self.assertRaises(SystemExit, loman.main, ['loman'])
+ self.assertTrue(sys.stderr.getvalue().endswith(err_msg))
+
+ def testNotWorkon(self):
+ err_msg = 'Adding of non-workon projects is currently unsupported.\n'
+ self.assertRaises(SystemExit, loman.main, ['loman', 'add', 'foo', 'path'])
+ self.assertTrue(sys.stderr.getvalue().endswith(err_msg))
+
+ def testBadCommand(self):
+ err_msg = 'Unsupported command: bad.\n'
+ self.assertRaises(SystemExit, loman.main, ['loman', 'bad', 'foo', 'path'])
+ self.assertTrue(sys.stderr.getvalue().endswith(err_msg))
+
+ def testSimpleAdd(self):
+ temp = tempfile.NamedTemporaryFile('w')
+ print >> temp, '\n'
+ temp.flush()
+ os.fsync(temp.fileno())
+ loman.main(['loman', 'add', '--workon', '-f',
+ temp.name, 'foo', 'path/to/foo'])
+ self.assertEqual(
+ open(temp.name, 'r').read(),
+ self.utf8 + '\n'
+ '\n'
+ '\n')
+
+ def testAddDup(self):
+ temp = tempfile.NamedTemporaryFile('w')
+ print >> temp, '\n'
+ temp.flush()
+ os.fsync(temp.fileno())
+ loman.main(['loman', 'add', '--workon', '-f',
+ temp.name, 'foo', 'path/to/foo'])
+ self.assertRaises(SystemExit, loman.main,
+ ['loman', 'add', '--workon', '-f',
+ temp.name, 'foo', 'path/to/foo'])
+
+
+if __name__ == '__main__':
+ unittest.main()