main/repo: add support for subcommand aliases

This supports [alias] sections with repo subcommands just like git.

Change-Id: Ie9235b5d4449414e6a745814f0110bd6af74ea93
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/255833
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
diff --git a/main.py b/main.py
index 2422e8b..06cd110 100755
--- a/main.py
+++ b/main.py
@@ -26,6 +26,7 @@
 import netrc
 import optparse
 import os
+import shlex
 import sys
 import textwrap
 import time
@@ -48,7 +49,7 @@
 import event_log
 from repo_trace import SetTrace
 from git_command import user_agent
-from git_config import init_ssh, close_ssh
+from git_config import init_ssh, close_ssh, RepoConfig
 from command import InteractiveCommand
 from command import MirrorSafeCommand
 from command import GitcAvailableCommand, GitcClientCommand
@@ -155,6 +156,9 @@
       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))
@@ -165,6 +169,27 @@
 
     return (name, gopts, argv)
 
+  def _ExpandAlias(self, name):
+    """Look up user registered aliases."""
+    # We don't resolve aliases for existing subcommands.  This matches git.
+    if name in self.commands:
+      return name, []
+
+    key = 'alias.%s' % (name,)
+    alias = RepoConfig.ForRepository(self.repodir).GetString(key)
+    if alias is None:
+      alias = RepoConfig.ForUser().GetString(key)
+    if alias is None:
+      return name, []
+
+    args = alias.strip().split(' ', 1)
+    name = args[0]
+    if len(args) == 2:
+      args = shlex.split(args[1])
+    else:
+      args = []
+    return name, args
+
   def _Run(self, name, gopts, argv):
     """Execute the requested subcommand."""
     result = 0
diff --git a/repo b/repo
index 77e8028..77a3f8d 100755
--- a/repo
+++ b/repo
@@ -13,6 +13,7 @@
 import datetime
 import os
 import platform
+import shlex
 import subprocess
 import sys
 
@@ -693,6 +694,24 @@
   run_git('config', name, value, cwd=cwd)
 
 
+def _GetRepoConfig(name):
+  """Read a repo configuration option."""
+  config = os.path.join(home_dot_repo, 'config')
+  if not os.path.exists(config):
+    return None
+
+  cmd = ['config', '--file', config, '--get', name]
+  ret = run_git(*cmd, check=False)
+  if ret.returncode == 0:
+    return ret.stdout
+  elif ret.returncode == 1:
+    return None
+  else:
+    print('repo: error: git %s failed:\n%s' % (' '.join(cmd), ret.stderr),
+          file=sys.stderr)
+    raise RunError()
+
+
 def _InitHttp():
   handlers = []
 
@@ -876,6 +895,25 @@
   version = False
 
 
+def _ExpandAlias(name):
+  """Look up user registered aliases."""
+  # We don't resolve aliases for existing subcommands.  This matches git.
+  if name in {'gitc-init', 'help', 'init'}:
+    return name, []
+
+  alias = _GetRepoConfig('alias.%s' % (name,))
+  if alias is None:
+    return name, []
+
+  args = alias.strip().split(' ', 1)
+  name = args[0]
+  if len(args) == 2:
+    args = shlex.split(args[1])
+  else:
+    args = []
+  return name, args
+
+
 def _ParseArguments(args):
   cmd = None
   opt = _Options()
@@ -1004,6 +1042,11 @@
           file=sys.stderr)
     sys.exit(1)
   if not repo_main:
+    # Only expand aliases here since we'll be parsing the CLI ourselves.
+    # If we had repo_main, alias expansion would happen in main.py.
+    cmd, alias_args = _ExpandAlias(cmd)
+    args = alias_args + args
+
     if opt.help:
       _Usage()
     if cmd == 'help':