From cf31fe9b4fb650b27e19f5d7ee7297e383660caf Mon Sep 17 00:00:00 2001 From: The Android Open Source Project Date: Tue, 21 Oct 2008 07:00:00 -0700 Subject: Initial Contribution --- subcmds/__init__.py | 49 ++++++++++ subcmds/compute_snapshot_check.py | 169 +++++++++++++++++++++++++++++++++ subcmds/diff.py | 27 ++++++ subcmds/forall.py | 82 ++++++++++++++++ subcmds/help.py | 147 +++++++++++++++++++++++++++++ subcmds/init.py | 193 ++++++++++++++++++++++++++++++++++++++ subcmds/prune.py | 59 ++++++++++++ subcmds/stage.py | 108 +++++++++++++++++++++ subcmds/start.py | 51 ++++++++++ subcmds/status.py | 27 ++++++ subcmds/sync.py | 150 +++++++++++++++++++++++++++++ subcmds/upload.py | 180 +++++++++++++++++++++++++++++++++++ 12 files changed, 1242 insertions(+) create mode 100644 subcmds/__init__.py create mode 100644 subcmds/compute_snapshot_check.py create mode 100644 subcmds/diff.py create mode 100644 subcmds/forall.py create mode 100644 subcmds/help.py create mode 100644 subcmds/init.py create mode 100644 subcmds/prune.py create mode 100644 subcmds/stage.py create mode 100644 subcmds/start.py create mode 100644 subcmds/status.py create mode 100644 subcmds/sync.py create mode 100644 subcmds/upload.py (limited to 'subcmds') diff --git a/subcmds/__init__.py b/subcmds/__init__.py new file mode 100644 index 00000000..a2286e78 --- /dev/null +++ b/subcmds/__init__.py @@ -0,0 +1,49 @@ +# +# Copyright (C) 2008 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +all = {} + +my_dir = os.path.dirname(__file__) +for py in os.listdir(my_dir): + if py == '__init__.py': + continue + + if py.endswith('.py'): + name = py[:-3] + + clsn = name.capitalize() + while clsn.find('_') > 0: + h = clsn.index('_') + clsn = clsn[0:h] + clsn[h + 1:].capitalize() + + mod = __import__(__name__, + globals(), + locals(), + ['%s' % name]) + mod = getattr(mod, name) + try: + cmd = getattr(mod, clsn)() + except AttributeError: + raise SyntaxError, '%s/%s does not define class %s' % ( + __name__, py, clsn) + + name = name.replace('_', '-') + cmd.NAME = name + all[name] = cmd + +if 'help' in all: + all['help'].commands = all diff --git a/subcmds/compute_snapshot_check.py b/subcmds/compute_snapshot_check.py new file mode 100644 index 00000000..82db359a --- /dev/null +++ b/subcmds/compute_snapshot_check.py @@ -0,0 +1,169 @@ +# +# Copyright (C) 2008 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import sys +import tempfile + +from command import Command +from error import GitError, NoSuchProjectError +from git_config import IsId +from import_tar import ImportTar +from import_zip import ImportZip +from project import Project +from remote import Remote + +def _ToCommit(project, rev): + return project.bare_git.rev_parse('--verify', '%s^0' % rev) + +def _Missing(project, rev): + return project._revlist('--objects', rev, '--not', '--all') + + +class ComputeSnapshotCheck(Command): + common = False + helpSummary = "Compute the check value for a new snapshot" + helpUsage = """ +%prog -p NAME -v VERSION -s FILE [options] +""" + helpDescription = """ +%prog computes and then displays the proper check value for a +snapshot, so it can be pasted into the manifest file for a project. +""" + + def _Options(self, p): + g = p.add_option_group('Snapshot description options') + g.add_option('-p', '--project', + dest='project', metavar='NAME', + help='destination project name') + g.add_option('-v', '--version', + dest='version', metavar='VERSION', + help='upstream version/revision identifier') + g.add_option('-s', '--snapshot', + dest='snapshot', metavar='PATH', + help='local tarball path') + g.add_option('--new-project', + dest='new_project', action='store_true', + help='destinition is a new project') + g.add_option('--keep', + dest='keep_git', action='store_true', + help='keep the temporary git repository') + + g = p.add_option_group('Base revision grafting options') + g.add_option('--prior', + dest='prior', metavar='COMMIT', + help='prior revision checksum') + + g = p.add_option_group('Path mangling options') + g.add_option('--strip-prefix', + dest='strip_prefix', metavar='PREFIX', + help='remove prefix from all paths on import') + g.add_option('--insert-prefix', + dest='insert_prefix', metavar='PREFIX', + help='insert prefix before all paths on import') + + + def _Compute(self, opt): + try: + real_project = self.GetProjects([opt.project])[0] + except NoSuchProjectError: + if opt.new_project: + print >>sys.stderr, \ + "warning: project '%s' does not exist" % opt.project + else: + raise NoSuchProjectError(opt.project) + + self._tmpdir = tempfile.mkdtemp() + project = Project(manifest = self.manifest, + name = opt.project, + remote = Remote('origin'), + gitdir = os.path.join(self._tmpdir, '.git'), + worktree = self._tmpdir, + relpath = opt.project, + revision = 'refs/heads/master') + project._InitGitDir() + + url = 'file://%s' % os.path.abspath(opt.snapshot) + + imp = None + for cls in [ImportTar, ImportZip]: + if cls.CanAccept(url): + imp = cls() + break + if not imp: + print >>sys.stderr, 'error: %s unsupported' % opt.snapshot + sys.exit(1) + + imp.SetProject(project) + imp.SetVersion(opt.version) + imp.AddUrl(url) + + if opt.prior: + if opt.new_project: + if not IsId(opt.prior): + print >>sys.stderr, 'error: --prior=%s not valid' % opt.prior + sys.exit(1) + else: + try: + opt.prior = _ToCommit(real_project, opt.prior) + missing = _Missing(real_project, opt.prior) + except GitError, e: + print >>sys.stderr,\ + 'error: --prior=%s not valid\n%s' \ + % (opt.prior, e) + sys.exit(1) + if missing: + print >>sys.stderr,\ + 'error: --prior=%s is valid, but is not reachable' \ + % opt.prior + sys.exit(1) + imp.SetParent(opt.prior) + + src = opt.strip_prefix + dst = opt.insert_prefix + if src or dst: + if src is None: + src = '' + if dst is None: + dst = '' + imp.RemapPath(src, dst) + commitId = imp.Import() + + print >>sys.stderr,"%s\t%s" % (commitId, imp.version) + return project + + def Execute(self, opt, args): + if args \ + or not opt.project \ + or not opt.version \ + or not opt.snapshot: + self.Usage() + + success = False + project = None + try: + self._tmpdir = None + project = self._Compute(opt) + finally: + if project and opt.keep_git: + print 'GIT_DIR = %s' % (project.gitdir) + elif self._tmpdir: + for root, dirs, files in os.walk(self._tmpdir, topdown=False): + for name in files: + os.remove(os.path.join(root, name)) + for name in dirs: + os.rmdir(os.path.join(root, name)) + os.rmdir(self._tmpdir) + diff --git a/subcmds/diff.py b/subcmds/diff.py new file mode 100644 index 00000000..e0247140 --- /dev/null +++ b/subcmds/diff.py @@ -0,0 +1,27 @@ +# +# Copyright (C) 2008 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from command import PagedCommand + +class Diff(PagedCommand): + common = True + helpSummary = "Show changes between commit and working tree" + helpUsage = """ +%prog [...] +""" + + def Execute(self, opt, args): + for project in self.GetProjects(args): + project.PrintWorkTreeDiff() diff --git a/subcmds/forall.py b/subcmds/forall.py new file mode 100644 index 00000000..b22e22a1 --- /dev/null +++ b/subcmds/forall.py @@ -0,0 +1,82 @@ +# +# Copyright (C) 2008 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import re +import os +import sys +import subprocess +from command import Command + +class Forall(Command): + common = False + helpSummary = "Run a shell command in each project" + helpUsage = """ +%prog [...] -c [...] +""" + helpDescription = """ +Executes the same shell command in each project. + +Environment +----------- +pwd is the project's working directory. + +REPO_PROJECT is set to the unique name of the project. + +shell positional arguments ($1, $2, .., $#) are set to any arguments +following . + +stdin, stdout, stderr are inherited from the terminal and are +not redirected. +""" + + def _Options(self, p): + def cmd(option, opt_str, value, parser): + setattr(parser.values, option.dest, list(parser.rargs)) + while parser.rargs: + del parser.rargs[0] + p.add_option('-c', '--command', + help='Command (and arguments) to execute', + dest='command', + action='callback', + callback=cmd) + + def Execute(self, opt, args): + if not opt.command: + self.Usage() + + cmd = [opt.command[0]] + + shell = True + if re.compile(r'^[a-z0-9A-Z_/\.-]+$').match(cmd[0]): + shell = False + + if shell: + cmd.append(cmd[0]) + cmd.extend(opt.command[1:]) + + rc = 0 + for project in self.GetProjects(args): + env = dict(os.environ.iteritems()) + env['REPO_PROJECT'] = project.name + + p = subprocess.Popen(cmd, + cwd = project.worktree, + shell = shell, + env = env) + r = p.wait() + if r != 0 and r != rc: + rc = r + if rc != 0: + sys.exit(rc) diff --git a/subcmds/help.py b/subcmds/help.py new file mode 100644 index 00000000..6e0238a0 --- /dev/null +++ b/subcmds/help.py @@ -0,0 +1,147 @@ +# +# Copyright (C) 2008 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys +from formatter import AbstractFormatter, DumbWriter + +from color import Coloring +from command import PagedCommand + +class Help(PagedCommand): + common = False + helpSummary = "Display detailed help on a command" + helpUsage = """ +%prog [--all|command] +""" + helpDescription = """ +Displays detailed usage information about a command. +""" + + def _PrintAllCommands(self): + print 'usage: repo COMMAND [ARGS]' + print """ +The complete list of recognized repo commands are: +""" + commandNames = self.commands.keys() + commandNames.sort() + + maxlen = 0 + for name in commandNames: + maxlen = max(maxlen, len(name)) + fmt = ' %%-%ds %%s' % maxlen + + for name in commandNames: + command = self.commands[name] + try: + summary = command.helpSummary.strip() + except AttributeError: + summary = '' + print fmt % (name, summary) + print """ +See 'repo help ' for more information on a specific command. +""" + + def _PrintCommonCommands(self): + print 'usage: repo COMMAND [ARGS]' + print """ +The most commonly used repo commands are: +""" + commandNames = [name + for name in self.commands.keys() + if self.commands[name].common] + commandNames.sort() + + maxlen = 0 + for name in commandNames: + maxlen = max(maxlen, len(name)) + fmt = ' %%-%ds %%s' % maxlen + + for name in commandNames: + command = self.commands[name] + try: + summary = command.helpSummary.strip() + except AttributeError: + summary = '' + print fmt % (name, summary) + print """ +See 'repo help ' for more information on a specific command. +""" + + def _PrintCommandHelp(self, cmd): + class _Out(Coloring): + def __init__(self, gc): + Coloring.__init__(self, gc, 'help') + self.heading = self.printer('heading', attr='bold') + + self.wrap = AbstractFormatter(DumbWriter()) + + def _PrintSection(self, heading, bodyAttr): + try: + body = getattr(cmd, bodyAttr) + except AttributeError: + return + + self.nl() + + self.heading('%s', heading) + self.nl() + + self.heading('%s', ''.ljust(len(heading), '-')) + self.nl() + + me = 'repo %s' % cmd.NAME + body = body.strip() + body = body.replace('%prog', me) + + for para in body.split("\n\n"): + if para.startswith(' '): + self.write('%s', para) + self.nl() + self.nl() + else: + self.wrap.add_flowing_data(para) + self.wrap.end_paragraph(1) + self.wrap.end_paragraph(0) + + out = _Out(self.manifest.globalConfig) + cmd.OptionParser.print_help() + out._PrintSection('Summary', 'helpSummary') + out._PrintSection('Description', 'helpDescription') + + def _Options(self, p): + p.add_option('-a', '--all', + dest='show_all', action='store_true', + help='show the complete list of commands') + + def Execute(self, opt, args): + if len(args) == 0: + if opt.show_all: + self._PrintAllCommands() + else: + self._PrintCommonCommands() + + elif len(args) == 1: + name = args[0] + + try: + cmd = self.commands[name] + except KeyError: + print >>sys.stderr, "repo: '%s' is not a repo command." % name + sys.exit(1) + + self._PrintCommandHelp(cmd) + + else: + self._PrintCommandHelp(self) diff --git a/subcmds/init.py b/subcmds/init.py new file mode 100644 index 00000000..03f358d1 --- /dev/null +++ b/subcmds/init.py @@ -0,0 +1,193 @@ +# +# Copyright (C) 2008 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import sys + +from color import Coloring +from command import InteractiveCommand +from error import ManifestParseError +from remote import Remote +from git_command import git, MIN_GIT_VERSION + +class Init(InteractiveCommand): + common = True + helpSummary = "Initialize repo in the current directory" + helpUsage = """ +%prog [options] +""" + helpDescription = """ +The '%prog' command is run once to install and initialize repo. +The latest repo source code and manifest collection is downloaded +from the server and is installed in the .repo/ directory in the +current working directory. + +The optional argument can be used to specify an alternate +manifest to be used. If no manifest is specified, the manifest +default.xml will be used. +""" + + def _Options(self, p): + # Logging + g = p.add_option_group('Logging options') + g.add_option('-q', '--quiet', + dest="quiet", action="store_true", default=False, + help="be quiet") + + # Manifest + g = p.add_option_group('Manifest options') + g.add_option('-u', '--manifest-url', + dest='manifest_url', + help='manifest repository location', metavar='URL') + g.add_option('-b', '--manifest-branch', + dest='manifest_branch', + help='manifest branch or revision', metavar='REVISION') + g.add_option('-m', '--manifest-name', + dest='manifest_name', default='default.xml', + help='initial manifest file', metavar='NAME.xml') + + # Tool + g = p.add_option_group('Version options') + g.add_option('--repo-url', + dest='repo_url', + help='repo repository location', metavar='URL') + g.add_option('--repo-branch', + dest='repo_branch', + help='repo branch or revision', metavar='REVISION') + g.add_option('--no-repo-verify', + dest='no_repo_verify', action='store_true', + help='do not verify repo source code') + + def _CheckGitVersion(self): + ver_str = git.version() + if not ver_str.startswith('git version '): + print >>sys.stderr, 'error: "%s" unsupported' % ver_str + sys.exit(1) + + ver_str = ver_str[len('git version '):].strip() + ver_act = tuple(map(lambda x: int(x), ver_str.split('.')[0:3])) + if ver_act < MIN_GIT_VERSION: + need = '.'.join(map(lambda x: str(x), MIN_GIT_VERSION)) + print >>sys.stderr, 'fatal: git %s or later required' % need + sys.exit(1) + + def _SyncManifest(self, opt): + m = self.manifest.manifestProject + + if not m.Exists: + if not opt.manifest_url: + print >>sys.stderr, 'fatal: manifest url (-u) is required.' + sys.exit(1) + + if not opt.quiet: + print >>sys.stderr, 'Getting manifest ...' + print >>sys.stderr, ' from %s' % opt.manifest_url + m._InitGitDir() + + if opt.manifest_branch: + m.revision = opt.manifest_branch + else: + m.revision = 'refs/heads/master' + else: + if opt.manifest_branch: + m.revision = opt.manifest_branch + else: + m.PreSync() + + if opt.manifest_url: + r = m.GetRemote(m.remote.name) + r.url = opt.manifest_url + r.ResetFetch() + r.Save() + + m.Sync_NetworkHalf() + m.Sync_LocalHalf() + m.StartBranch('default') + + def _LinkManifest(self, name): + if not name: + print >>sys.stderr, 'fatal: manifest name (-m) is required.' + sys.exit(1) + + try: + self.manifest.Link(name) + except ManifestParseError, e: + print >>sys.stderr, "fatal: manifest '%s' not available" % name + print >>sys.stderr, 'fatal: %s' % str(e) + sys.exit(1) + + def _PromptKey(self, prompt, key, value): + mp = self.manifest.manifestProject + + sys.stdout.write('%-10s [%s]: ' % (prompt, value)) + a = sys.stdin.readline().strip() + if a != '' and a != value: + mp.config.SetString(key, a) + + def _ConfigureUser(self): + mp = self.manifest.manifestProject + + print '' + self._PromptKey('Your Name', 'user.name', mp.UserName) + self._PromptKey('Your Email', 'user.email', mp.UserEmail) + + def _HasColorSet(self, gc): + for n in ['ui', 'diff', 'status']: + if gc.Has('color.%s' % n): + return True + return False + + def _ConfigureColor(self): + gc = self.manifest.globalConfig + if self._HasColorSet(gc): + return + + class _Test(Coloring): + def __init__(self): + Coloring.__init__(self, gc, 'test color display') + self._on = True + out = _Test() + + print '' + print "Testing colorized output (for 'repo diff', 'repo status'):" + + for c in ['black','red','green','yellow','blue','magenta','cyan']: + out.write(' ') + out.printer(fg=c)(' %-6s ', c) + out.write(' ') + out.printer(fg='white', bg='black')(' %s ' % 'white') + out.nl() + + for c in ['bold','dim','ul','reverse']: + out.write(' ') + out.printer(fg='black', attr=c)(' %-6s ', c) + out.nl() + + sys.stdout.write('Enable color display in this user account (y/n)? ') + a = sys.stdin.readline().strip().lower() + if a in ('y', 'yes', 't', 'true', 'on'): + gc.SetString('color.ui', 'auto') + + def Execute(self, opt, args): + self._CheckGitVersion() + self._SyncManifest(opt) + self._LinkManifest(opt.manifest_name) + + if os.isatty(0) and os.isatty(1): + self._ConfigureUser() + self._ConfigureColor() + + print '' + print 'repo initialized in %s' % self.manifest.topdir diff --git a/subcmds/prune.py b/subcmds/prune.py new file mode 100644 index 00000000..f412bd48 --- /dev/null +++ b/subcmds/prune.py @@ -0,0 +1,59 @@ +# +# Copyright (C) 2008 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from color import Coloring +from command import PagedCommand + +class Prune(PagedCommand): + common = True + helpSummary = "Prune (delete) already merged topics" + helpUsage = """ +%prog [...] +""" + + def Execute(self, opt, args): + all = [] + for project in self.GetProjects(args): + all.extend(project.PruneHeads()) + + if not all: + return + + class Report(Coloring): + def __init__(self, config): + Coloring.__init__(self, config, 'status') + self.project = self.printer('header', attr='bold') + + out = Report(all[0].project.config) + out.project('Pending Branches') + out.nl() + + project = None + + for branch in all: + if project != branch.project: + project = branch.project + out.nl() + out.project('project %s/' % project.relpath) + out.nl() + + commits = branch.commits + date = branch.date + print '%s %-33s (%2d commit%s, %s)' % ( + branch.name == project.CurrentBranch and '*' or ' ', + branch.name, + len(commits), + len(commits) != 1 and 's' or ' ', + date) diff --git a/subcmds/stage.py b/subcmds/stage.py new file mode 100644 index 00000000..c451cd6d --- /dev/null +++ b/subcmds/stage.py @@ -0,0 +1,108 @@ +# +# Copyright (C) 2008 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys + +from color import Coloring +from command import InteractiveCommand +from git_command import GitCommand + +class _ProjectList(Coloring): + def __init__(self, gc): + Coloring.__init__(self, gc, 'interactive') + self.prompt = self.printer('prompt', fg='blue', attr='bold') + self.header = self.printer('header', attr='bold') + self.help = self.printer('help', fg='red', attr='bold') + +class Stage(InteractiveCommand): + common = True + helpSummary = "Stage file(s) for commit" + helpUsage = """ +%prog -i [...] +""" + helpDescription = """ +The '%prog' command stages files to prepare the next commit. +""" + + def _Options(self, p): + p.add_option('-i', '--interactive', + dest='interactive', action='store_true', + help='use interactive staging') + + def Execute(self, opt, args): + if opt.interactive: + self._Interactive(opt, args) + else: + self.Usage() + + def _Interactive(self, opt, args): + all = filter(lambda x: x.IsDirty(), self.GetProjects(args)) + if not all: + print >>sys.stderr,'no projects have uncommitted modifications' + return + + out = _ProjectList(self.manifest.manifestProject.config) + while True: + out.header(' %-20s %s', 'project', 'path') + out.nl() + + for i in xrange(0, len(all)): + p = all[i] + out.write('%3d: %-20s %s', i + 1, p.name, p.relpath + '/') + out.nl() + out.nl() + + out.write('%3d: (', 0) + out.prompt('q') + out.write('uit)') + out.nl() + + out.prompt('project> ') + try: + a = sys.stdin.readline() + except KeyboardInterrupt: + out.nl() + break + if a == '': + out.nl() + break + + a = a.strip() + if a.lower() in ('q', 'quit', 'exit'): + break + if not a: + continue + + try: + a_index = int(a) + except ValueError: + a_index = None + + if a_index is not None: + if a_index == 0: + break + if 0 < a_index and a_index <= len(all): + _AddI(all[a_index - 1]) + continue + + p = filter(lambda x: x.name == a or x.relpath == a, all) + if len(p) == 1: + _AddI(p[0]) + continue + print 'Bye.' + +def _AddI(project): + p = GitCommand(project, ['add', '--interactive'], bare=False) + p.Wait() diff --git a/subcmds/start.py b/subcmds/start.py new file mode 100644 index 00000000..4eb3e476 --- /dev/null +++ b/subcmds/start.py @@ -0,0 +1,51 @@ +# +# Copyright (C) 2008 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys +from command import Command +from git_command import git + +class Start(Command): + common = True + helpSummary = "Start a new branch for development" + helpUsage = """ +%prog [...] + +This subcommand starts a new branch of development that is automatically +pulled from a remote branch. + +It is equivalent to the following git commands: + +"git branch --track m/", +or +"git checkout --track -b m/". + +All three forms set up the config entries that repo bases some of its +processing on. Use %prog or git branch or checkout with --track to ensure +the configuration data is set up properly. + +""" + + def Execute(self, opt, args): + if not args: + self.Usage() + + nb = args[0] + if not git.check_ref_format('heads/%s' % nb): + print >>sys.stderr, "error: '%s' is not a valid name" % nb + sys.exit(1) + + for project in self.GetProjects(args[1:]): + project.StartBranch(nb) diff --git a/subcmds/status.py b/subcmds/status.py new file mode 100644 index 00000000..1615b423 --- /dev/null +++ b/subcmds/status.py @@ -0,0 +1,27 @@ +# +# Copyright (C) 2008 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from command import PagedCommand + +class Status(PagedCommand): + common = True + helpSummary = "Show the working tree status" + helpUsage = """ +%prog [...] +""" + + def Execute(self, opt, args): + for project in self.GetProjects(args): + project.PrintWorkTreeStatus() diff --git a/subcmds/sync.py b/subcmds/sync.py new file mode 100644 index 00000000..3eb44edf --- /dev/null +++ b/subcmds/sync.py @@ -0,0 +1,150 @@ +# +# Copyright (C) 2008 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import re +import subprocess +import sys + +from git_command import GIT +from command import Command +from error import RepoChangedException, GitError +from project import R_HEADS + +class Sync(Command): + common = True + helpSummary = "Update working tree to the latest revision" + helpUsage = """ +%prog [...] +""" + helpDescription = """ +The '%prog' command synchronizes local project directories +with the remote repositories specified in the manifest. If a local +project does not yet exist, it will clone a new local directory from +the remote repository and set up tracking branches as specified in +the manifest. If the local project already exists, '%prog' +will update the remote branches and rebase any new local changes +on top of the new remote changes. + +'%prog' will synchronize all projects listed at the command +line. Projects can be specified either by name, or by a relative +or absolute path to the project's local directory. If no projects +are specified, '%prog' will synchronize all projects listed in +the manifest. +""" + + def _Options(self, p): + p.add_option('--no-repo-verify', + dest='no_repo_verify', action='store_true', + help='do not verify repo source code') + + def _Fetch(self, *projects): + fetched = set() + for project in projects: + if project.Sync_NetworkHalf(): + fetched.add(project.gitdir) + else: + print >>sys.stderr, 'error: Cannot fetch %s' % project.name + sys.exit(1) + return fetched + + def Execute(self, opt, args): + rp = self.manifest.repoProject + rp.PreSync() + + mp = self.manifest.manifestProject + mp.PreSync() + + all = self.GetProjects(args, missing_ok=True) + fetched = self._Fetch(rp, mp, *all) + + if rp.HasChanges: + print >>sys.stderr, 'info: A new version of repo is available' + print >>sys.stderr, '' + if opt.no_repo_verify or _VerifyTag(rp): + if not rp.Sync_LocalHalf(): + sys.exit(1) + print >>sys.stderr, 'info: Restarting repo with latest version' + raise RepoChangedException() + else: + print >>sys.stderr, 'warning: Skipped upgrade to unverified version' + + if mp.HasChanges: + if not mp.Sync_LocalHalf(): + sys.exit(1) + + self.manifest._Unload() + all = self.GetProjects(args, missing_ok=True) + missing = [] + for project in all: + if project.gitdir not in fetched: + missing.append(project) + self._Fetch(*missing) + + for project in all: + if not project.Sync_LocalHalf(): + sys.exit(1) + + +def _VerifyTag(project): + gpg_dir = os.path.expanduser('~/.repoconfig/gnupg') + if not os.path.exists(gpg_dir): + print >>sys.stderr,\ +"""warning: GnuPG was not available during last "repo init" +warning: Cannot automatically authenticate repo.""" + return True + + remote = project.GetRemote(project.remote.name) + ref = remote.ToLocal(project.revision) + + try: + cur = project.bare_git.describe(ref) + except GitError: + cur = None + + if not cur \ + or re.compile(r'^.*-[0-9]{1,}-g[0-9a-f]{1,}$').match(cur): + rev = project.revision + if rev.startswith(R_HEADS): + rev = rev[len(R_HEADS):] + + print >>sys.stderr + print >>sys.stderr,\ + "warning: project '%s' branch '%s' is not signed" \ + % (project.name, rev) + return False + + env = dict(os.environ) + env['GIT_DIR'] = project.gitdir + env['GNUPGHOME'] = gpg_dir + + cmd = [GIT, 'tag', '-v', cur] + proc = subprocess.Popen(cmd, + stdout = subprocess.PIPE, + stderr = subprocess.PIPE, + env = env) + out = proc.stdout.read() + proc.stdout.close() + + err = proc.stderr.read() + proc.stderr.close() + + if proc.wait() != 0: + print >>sys.stderr + print >>sys.stderr, out + print >>sys.stderr, err + print >>sys.stderr + return False + return True diff --git a/subcmds/upload.py b/subcmds/upload.py new file mode 100644 index 00000000..ad05050e --- /dev/null +++ b/subcmds/upload.py @@ -0,0 +1,180 @@ +# +# Copyright (C) 2008 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import re +import sys + +from command import InteractiveCommand +from editor import Editor +from error import UploadError + +def _die(fmt, *args): + msg = fmt % args + print >>sys.stderr, 'error: %s' % msg + sys.exit(1) + +class Upload(InteractiveCommand): + common = True + helpSummary = "Upload changes for code review" + helpUsage=""" +%prog []... +""" + helpDescription = """ +The '%prog' command is used to send changes to the Gerrit code +review system. It searches for changes in local projects that do +not yet exist in the corresponding remote repository. If multiple +changes are found, '%prog' opens an editor to allow the +user to choose which change to upload. After a successful upload, +repo prints the URL for the change in the Gerrit code review system. + +'%prog' searches for uploadable changes in all projects listed +at the command line. Projects can be specified either by name, or +by a relative or absolute path to the project's local directory. If +no projects are specified, '%prog' will search for uploadable +changes in all projects listed in the manifest. +""" + + def _SingleBranch(self, branch): + project = branch.project + name = branch.name + date = branch.date + list = branch.commits + + print 'Upload project %s/:' % project.relpath + print ' branch %s (%2d commit%s, %s):' % ( + name, + len(list), + len(list) != 1 and 's' or '', + date) + for commit in list: + print ' %s' % commit + + sys.stdout.write('(y/n)? ') + answer = sys.stdin.readline().strip() + if answer in ('y', 'Y', 'yes', '1', 'true', 't'): + self._UploadAndReport([branch]) + else: + _die("upload aborted by user") + + def _MultipleBranches(self, pending): + projects = {} + branches = {} + + script = [] + script.append('# Uncomment the branches to upload:') + for project, avail in pending: + script.append('#') + script.append('# project %s/:' % project.relpath) + + b = {} + for branch in avail: + name = branch.name + date = branch.date + list = branch.commits + + if b: + script.append('#') + script.append('# branch %s (%2d commit%s, %s):' % ( + name, + len(list), + len(list) != 1 and 's' or '', + date)) + for commit in list: + script.append('# %s' % commit) + b[name] = branch + + projects[project.relpath] = project + branches[project.name] = b + script.append('') + + script = Editor.EditString("\n".join(script)).split("\n") + + project_re = re.compile(r'^#?\s*project\s*([^\s]+)/:$') + branch_re = re.compile(r'^\s*branch\s*([^\s(]+)\s*\(.*') + + project = None + todo = [] + + for line in script: + m = project_re.match(line) + if m: + name = m.group(1) + project = projects.get(name) + if not project: + _die('project %s not available for upload', name) + continue + + m = branch_re.match(line) + if m: + name = m.group(1) + if not project: + _die('project for branch %s not in script', name) + branch = branches[project.name].get(name) + if not branch: + _die('branch %s not in %s', name, project.relpath) + todo.append(branch) + if not todo: + _die("nothing uncommented for upload") + self._UploadAndReport(todo) + + def _UploadAndReport(self, todo): + have_errors = False + for branch in todo: + try: + branch.UploadForReview() + branch.uploaded = True + except UploadError, e: + branch.error = e + branch.uploaded = False + have_errors = True + + print >>sys.stderr, '' + print >>sys.stderr, '--------------------------------------------' + + if have_errors: + for branch in todo: + if not branch.uploaded: + print >>sys.stderr, '[FAILED] %-15s %-15s (%s)' % ( + branch.project.relpath + '/', \ + branch.name, \ + branch.error) + print >>sys.stderr, '' + + for branch in todo: + if branch.uploaded: + print >>sys.stderr, '[OK ] %-15s %s' % ( + branch.project.relpath + '/', + branch.name) + print >>sys.stderr, '%s' % branch.tip_url + print >>sys.stderr, '' + + if have_errors: + sys.exit(1) + + def Execute(self, opt, args): + project_list = self.GetProjects(args) + pending = [] + + for project in project_list: + avail = project.GetUploadableBranches() + if avail: + pending.append((project, avail)) + + if not pending: + print >>sys.stdout, "no branches ready for upload" + elif len(pending) == 1 and len(pending[0][1]) == 1: + self._SingleBranch(pending[0][1][0]) + else: + self._MultipleBranches(pending) -- cgit v1.2.3-54-g00ecf