diff --git a/completion.bash b/completion.bash index 0b52d29c..09291d5c 100644 --- a/completion.bash +++ b/completion.bash @@ -14,6 +14,9 @@ # Programmable bash completion. https://github.com/scop/bash-completion +# TODO: Handle interspersed options. We handle `repo h<tab>`, but not +# `repo --time h<tab>`. + # Complete the list of repo subcommands. __complete_repo_list_commands() { local repo=${COMP_WORDS[0]} @@ -37,6 +40,7 @@ __complete_repo_list_branches() { __complete_repo_list_projects() { local repo=${COMP_WORDS[0]} "${repo}" list -n 2>/dev/null + "${repo}" list -p --relative-to=. 2>/dev/null } # Complete the repo <command> argument. @@ -66,6 +70,48 @@ __complete_repo_command_projects() { COMPREPLY=($(compgen -W "$(__complete_repo_list_projects)" -- "${current}")) } +# Complete `repo help`. +__complete_repo_command_help() { + local current=$1 + # CWORD=1 is "start". + # CWORD=2 is the <subcommand> which we complete here. + if [[ ${COMP_CWORD} -eq 2 ]]; then + COMPREPLY=( + $(compgen -W "$(__complete_repo_list_commands)" -- "${current}") + ) + fi +} + +# Complete `repo forall`. +__complete_repo_command_forall() { + local current=$1 + # CWORD=1 is "forall". + # CWORD=2+ are <projects> *until* we hit the -c option. + local i + for (( i = 0; i < COMP_CWORD; ++i )); do + if [[ "${COMP_WORDS[i]}" == "-c" ]]; then + return 0 + fi + done + + COMPREPLY=( + $(compgen -W "$(__complete_repo_list_projects)" -- "${current}") + ) +} + +# Complete `repo start`. +__complete_repo_command_start() { + local current=$1 + # CWORD=1 is "start". + # CWORD=2 is the <branch> which we don't complete. + # CWORD=3+ are <projects> which we complete here. + if [[ ${COMP_CWORD} -gt 2 ]]; then + COMPREPLY=( + $(compgen -W "$(__complete_repo_list_projects)" -- "${current}") + ) + fi +} + # Complete the repo subcommand arguments. __complete_repo_arg() { if [[ ${COMP_CWORD} -le 1 ]]; then @@ -86,21 +132,8 @@ __complete_repo_arg() { return 0 ;; - help) - if [[ ${COMP_CWORD} -eq 2 ]]; then - COMPREPLY=( - $(compgen -W "$(__complete_repo_list_commands)" -- "${current}") - ) - fi - return 0 - ;; - - start) - if [[ ${COMP_CWORD} -gt 2 ]]; then - COMPREPLY=( - $(compgen -W "$(__complete_repo_list_projects)" -- "${current}") - ) - fi + help|start|forall) + __complete_repo_command_${command} "${current}" return 0 ;; @@ -118,4 +151,6 @@ __complete_repo() { return 0 } -complete -F __complete_repo repo +# Fallback to the default complete methods if we aren't able to provide anything +# useful. This will allow e.g. local paths to be used when it makes sense. +complete -F __complete_repo -o bashdefault -o default repo diff --git a/git_config.py b/git_config.py index 4416bfa9..01183a0a 100644 --- a/git_config.py +++ b/git_config.py @@ -31,7 +31,8 @@ from repo_trace import Trace from git_command import GitCommand from git_refs import R_CHANGES, R_HEADS, R_TAGS -# Prefix Add all the data of SyncAnalysisState object. +# Prefix that is prepended to all the keys of SyncAnalysisState's data +# that is saved in the config. SYNC_STATE_PREFIX = 'syncstate.' ID_RE = re.compile(r'^[0-9a-f]{40}$') @@ -271,10 +272,7 @@ class GitConfig(object): return {k: v for k, v in self.DumpConfigDict().items() if k.startswith(SYNC_STATE_PREFIX)} def UpdateSyncAnalysisState(self, options, superproject_logging_data): - """Update Config's SyncAnalysisState with the latest sync data. - - Creates SyncAnalysisState object with |options| and |superproject_logging_data| - which in turn persists the data into the |self| object. + """Update Config's SYNC_STATE_PREFIX* data with the latest sync data. Args: options: Options passed to sync returned from optparse. See _Options(). @@ -743,7 +741,7 @@ class Branch(object): class SyncAnalysisState(): - """Configuration options related to logging of Sync state for analysis. + """Configuration options related to logging of sync state for analysis. This object is versioned. """ @@ -786,7 +784,7 @@ class SyncAnalysisState(): def _Set(self, key, value): """Set the |value| for a |key| in the |_config| member. - Every key is stored by prepending the value of SYNC_STATE_PREFIX constant to it. + |key| is prepended with the value of SYNC_STATE_PREFIX constant. Args: key: Name of the key. diff --git a/git_superproject.py b/git_superproject.py index 86100960..8769355c 100644 --- a/git_superproject.py +++ b/git_superproject.py @@ -106,6 +106,11 @@ class Superproject(object): """Returns a dictionary of projects and their commit ids.""" return self._project_commit_ids + @property + def manifest_path(self): + """Returns the manifest path if the path exists or None.""" + return self._manifest_path if os.path.exists(self._manifest_path) else None + def _GetBranch(self): """Returns the branch name for getting the approved manifest.""" p = self._manifest.manifestProject diff --git a/main.py b/main.py index f6631f5f..2050cabb 100755 --- a/main.py +++ b/main.py @@ -95,6 +95,8 @@ global_options = optparse.OptionParser( add_help_option=False) global_options.add_option('-h', '--help', action='store_true', help='show this help message and exit') +global_options.add_option('--help-all', action='store_true', + help='show this help message with all subcommands and exit') global_options.add_option('-p', '--paginate', dest='pager', action='store_true', help='display command output in the pager') @@ -116,6 +118,10 @@ global_options.add_option('--time', global_options.add_option('--version', dest='show_version', action='store_true', help='display this version of repo') +global_options.add_option('--show-toplevel', + action='store_true', + help='display the path of the top-level directory of ' + 'the repo client checkout') global_options.add_option('--event-log', dest='event_log', action='store', help='filename of event log to append timeline to') @@ -128,34 +134,40 @@ class _Repo(object): self.repodir = repodir self.commands = all_commands + def _PrintHelp(self, short: bool = False, all_commands: bool = False): + """Show --help screen.""" + global_options.print_help() + print() + if short: + commands = ' '.join(sorted(self.commands)) + wrapped_commands = textwrap.wrap(commands, width=77) + print('Available commands:\n %s' % ('\n '.join(wrapped_commands),)) + print('\nRun `repo help <command>` for command-specific details.') + print('Bug reports:', Wrapper().BUG_URL) + else: + cmd = self.commands['help']() + if all_commands: + cmd.PrintAllCommandsBody() + else: + cmd.PrintCommonCommandsBody() + def _ParseArgs(self, argv): """Parse the main `repo` command line options.""" - name = None - glob = [] - - for i in range(len(argv)): - if not argv[i].startswith('-'): - name = argv[i] - if i > 0: - glob = argv[:i] + for i, arg in enumerate(argv): + if not arg.startswith('-'): + name = arg + glob = argv[:i] argv = argv[i + 1:] break - if not name: + else: + name = None glob = argv - name = 'help' argv = [] gopts, _gargs = global_options.parse_args(glob) - name, alias_args = self._ExpandAlias(name) - argv = alias_args + argv - - if gopts.help: - global_options.print_help() - commands = ' '.join(sorted(self.commands)) - wrapped_commands = textwrap.wrap(commands, width=77) - print('\nAvailable commands:\n %s' % ('\n '.join(wrapped_commands),)) - print('\nRun `repo help <command>` for command-specific details.') - global_options.exit() + if name: + name, alias_args = self._ExpandAlias(name) + argv = alias_args + argv return (name, gopts, argv) @@ -186,12 +198,21 @@ class _Repo(object): if gopts.trace: SetTrace() - if gopts.show_version: - if name == 'help': - name = 'version' - else: - print('fatal: invalid usage of --version', file=sys.stderr) - return 1 + + # Handle options that terminate quickly first. + if gopts.help or gopts.help_all: + self._PrintHelp(short=False, all_commands=gopts.help_all) + return 0 + elif gopts.show_version: + # Always allow global --version regardless of subcommand validity. + name = 'version' + elif gopts.show_toplevel: + print(os.path.dirname(self.repodir)) + return 0 + elif not name: + # No subcommand specified, so show the help/subcommand. + self._PrintHelp(short=True) + return 1 SetDefaultColoring(gopts.color) diff --git a/man/repo-list.1 b/man/repo-list.1 index a86315ae..7f85e612 100644 --- a/man/repo-list.1 +++ b/man/repo-list.1 @@ -27,15 +27,19 @@ project is in \fB\-a\fR, \fB\-\-all\fR show projects regardless of checkout state .TP -\fB\-f\fR, \fB\-\-fullpath\fR -display the full work tree path instead of the -relative path -.TP \fB\-n\fR, \fB\-\-name\-only\fR display only the name of the repository .TP \fB\-p\fR, \fB\-\-path\-only\fR display only the path of the repository +.TP +\fB\-f\fR, \fB\-\-fullpath\fR +display the full work tree path instead of the +relative path +.TP +\fB\-\-relative\-to\fR=\fI\,PATH\/\fR +display paths relative to this one (default: top of +repo client checkout) .SS Logging options: .TP \fB\-v\fR, \fB\-\-verbose\fR diff --git a/man/repo-manifest.1 b/man/repo-manifest.1 index e42cc42e..be467607 100644 --- a/man/repo-manifest.1 +++ b/man/repo-manifest.1 @@ -36,6 +36,9 @@ output manifest in JSON format (experimental) \fB\-\-pretty\fR format output for humans to read .TP +\fB\-\-no\-local\-manifests\fR +ignore local manifests +.TP \fB\-o\fR \-|NAME.xml, \fB\-\-output\-file\fR=\fI\,\-\/\fR|NAME.xml file to save the manifest to .SS Logging options: @@ -95,7 +98,7 @@ include*)> .IP <!ELEMENT notice (#PCDATA)> .IP -<!ELEMENT remote EMPTY> +<!ELEMENT remote (annotation*)> <!ATTLIST remote name ID #REQUIRED> <!ATTLIST remote alias CDATA #IMPLIED> <!ATTLIST remote fetch CDATA #REQUIRED> @@ -393,13 +396,13 @@ Same syntax as the corresponding element of `project`. .PP Element annotation .PP -Zero or more annotation elements may be specified as children of a project -element. Each element describes a name\-value pair that will be exported into -each project's environment during a 'forall' command, prefixed with REPO__. In -addition, there is an optional attribute "keep" which accepts the case -insensitive values "true" (default) or "false". This attribute determines -whether or not the annotation will be kept when exported with the manifest -subcommand. +Zero or more annotation elements may be specified as children of a project or +remote element. Each element describes a name\-value pair. For projects, this +name\-value pair will be exported into each project's environment during a +\&'forall' command, prefixed with `REPO__`. In addition, there is an optional +attribute "keep" which accepts the case insensitive values "true" (default) or +"false". This attribute determines whether or not the annotation will be kept +when exported with the manifest subcommand. .PP Element copyfile .PP diff --git a/man/repo.1 b/man/repo.1 index 0bc3acdb..4aa76380 100644 --- a/man/repo.1 +++ b/man/repo.1 @@ -2,9 +2,48 @@ .TH REPO "1" "July 2021" "repo" "Repo Manual" .SH NAME repo \- repository management tool built on top of git -.SH DESCRIPTION -usage: repo COMMAND [ARGS] -The complete list of recognized repo commands are: +.SH SYNOPSIS +.B repo +[\fI\,-p|--paginate|--no-pager\/\fR] \fI\,COMMAND \/\fR[\fI\,ARGS\/\fR] +.SH OPTIONS +.TP +\fB\-h\fR, \fB\-\-help\fR +show this help message and exit +.TP +\fB\-\-help\-all\fR +show this help message with all subcommands and exit +.TP +\fB\-p\fR, \fB\-\-paginate\fR +display command output in the pager +.TP +\fB\-\-no\-pager\fR +disable the pager +.TP +\fB\-\-color\fR=\fI\,COLOR\/\fR +control color usage: auto, always, never +.TP +\fB\-\-trace\fR +trace git command execution (REPO_TRACE=1) +.TP +\fB\-\-trace\-python\fR +trace python command execution +.TP +\fB\-\-time\fR +time repo command execution +.TP +\fB\-\-version\fR +display this version of repo +.TP +\fB\-\-show\-toplevel\fR +display the path of the top\-level directory of the +repo client checkout +.TP +\fB\-\-event\-log\fR=\fI\,EVENT_LOG\/\fR +filename of event log to append timeline to +.TP +\fB\-\-git\-trace2\-event\-log\fR=\fI\,GIT_TRACE2_EVENT_LOG\/\fR +directory to write git trace2 event log to +.SS "The complete list of recognized repo commands are:" .TP abandon Permanently abandon a development branch @@ -91,3 +130,4 @@ version Display the version of repo .PP See 'repo help <command>' for more information on a specific command. +Bug reports: https://bugs.chromium.org/p/gerrit/issues/entry?template=Repo+tool+issue diff --git a/release/update-manpages b/release/update-manpages index 3aeee206..f841f306 100755 --- a/release/update-manpages +++ b/release/update-manpages @@ -59,7 +59,7 @@ def main(argv): cmdlist.append(['help2man', '-N', '-n', 'repository management tool built on top of git', '-S', 'repo', '-m', 'Repo Manual', f'--version-string={version}', '-o', MANDIR.joinpath('repo.1'), TOPDIR.joinpath('repo'), - '-h', 'help --all']) + '-h', '--help-all']) with tempfile.TemporaryDirectory() as tempdir: repo_dir = Path(tempdir) / '.repo' diff --git a/subcmds/help.py b/subcmds/help.py index f302e75c..1a60ef45 100644 --- a/subcmds/help.py +++ b/subcmds/help.py @@ -50,14 +50,21 @@ Displays detailed usage information about a command. def _PrintAllCommands(self): print('usage: repo COMMAND [ARGS]') + self.PrintAllCommandsBody() + + def PrintAllCommandsBody(self): print('The complete list of recognized repo commands are:') commandNames = list(sorted(all_commands)) self._PrintCommands(commandNames) print("See 'repo help <command>' for more information on a " 'specific command.') + print('Bug reports:', Wrapper().BUG_URL) def _PrintCommonCommands(self): print('usage: repo COMMAND [ARGS]') + self.PrintCommonCommandsBody() + + def PrintCommonCommandsBody(self): print('The most commonly used repo commands are:') def gitc_supported(cmd): diff --git a/subcmds/list.py b/subcmds/list.py index 8d0c5640..6adf85b7 100644 --- a/subcmds/list.py +++ b/subcmds/list.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os + from command import Command, MirrorSafeCommand @@ -43,20 +45,26 @@ This is similar to running: repo forall -c 'echo "$REPO_PATH : $REPO_PROJECT"'. p.add_option('-a', '--all', action='store_true', help='show projects regardless of checkout state') - p.add_option('-f', '--fullpath', - dest='fullpath', action='store_true', - help='display the full work tree path instead of the relative path') p.add_option('-n', '--name-only', dest='name_only', action='store_true', help='display only the name of the repository') p.add_option('-p', '--path-only', dest='path_only', action='store_true', help='display only the path of the repository') + p.add_option('-f', '--fullpath', + dest='fullpath', action='store_true', + help='display the full work tree path instead of the relative path') + p.add_option('--relative-to', metavar='PATH', + help='display paths relative to this one (default: top of repo client checkout)') def ValidateOptions(self, opt, args): if opt.fullpath and opt.name_only: self.OptionParser.error('cannot combine -f and -n') + # Resolve any symlinks so the output is stable. + if opt.relative_to: + opt.relative_to = os.path.realpath(opt.relative_to) + def Execute(self, opt, args): """List all projects and the associated directories. @@ -76,6 +84,8 @@ This is similar to running: repo forall -c 'echo "$REPO_PATH : $REPO_PROJECT"'. def _getpath(x): if opt.fullpath: return x.worktree + if opt.relative_to: + return os.path.relpath(x.worktree, opt.relative_to) return x.relpath lines = [] diff --git a/subcmds/sync.py b/subcmds/sync.py index 11f71bf1..36b15bf0 100644 --- a/subcmds/sync.py +++ b/subcmds/sync.py @@ -302,6 +302,12 @@ later is required to fix a server side protocol bug. self.repodir, self.git_event_log, quiet=opt.quiet) + if opt.local_only: + manifest_path = superproject.manifest_path + if manifest_path: + self._ReloadManifest(manifest_path, load_local_manifests) + return manifest_path + all_projects = self.GetProjects(args, missing_ok=True, submodules_ok=opt.fetch_submodules) @@ -1080,11 +1086,11 @@ later is required to fix a server side protocol bug. file=sys.stderr) sys.exit(1) - # Log the previous sync state from the config. + # Log the previous sync analysis state from the config. self.git_event_log.LogConfigEvents(mp.config.GetSyncAnalysisStateData(), 'previous_sync_state') - # Update and log with the new sync state. + # Update and log with the new sync analysis state. mp.config.UpdateSyncAnalysisState(opt, superproject_logging_data) self.git_event_log.LogConfigEvents(mp.config.GetSyncAnalysisStateData(), 'current_sync_state') diff --git a/tests/fixtures/test.gitconfig b/tests/fixtures/test.gitconfig index 002a4a33..958fb205 100644 --- a/tests/fixtures/test.gitconfig +++ b/tests/fixtures/test.gitconfig @@ -12,7 +12,7 @@ intm = 10m intg = 10g [syncstate "main"] - synctime = 2021-07-28T19:42:03.866355Z + synctime = 2021-07-28T21:16:10.873226Z version = 1 [syncstate "sys"] argv = ['/usr/bin/pytest-3']