]> source.dussan.org Git - gitblit.git/commitdiff
Implement management commands in repositories dispatcher
authorJames Moger <james.moger@gitblit.com>
Fri, 28 Mar 2014 23:44:48 +0000 (19:44 -0400)
committerJames Moger <james.moger@gitblit.com>
Thu, 10 Apr 2014 23:00:52 +0000 (19:00 -0400)
src/main/java/com/gitblit/GitBlit.java
src/main/java/com/gitblit/models/UserModel.java
src/main/java/com/gitblit/transport/ssh/WelcomeShell.java
src/main/java/com/gitblit/transport/ssh/gitblit/BaseKeyCommand.java
src/main/java/com/gitblit/transport/ssh/gitblit/KeysDispatcher.java
src/main/java/com/gitblit/transport/ssh/gitblit/RepositoriesDispatcher.java
src/site/setup_transport_ssh.mkd

index 430ae6d1c4874f1923114af4476d25fd7317d613..26ab3f3be891cf90a2b927aa7daa6f3cb2203f27 100644 (file)
@@ -226,7 +226,7 @@ public class GitBlit extends GitblitManager {
        public boolean deleteRepositoryModel(RepositoryModel model) {
                boolean success = repositoryManager.deleteRepositoryModel(model);
                if (success && ticketService != null) {
-                       return ticketService.deleteAll(model);
+                       ticketService.deleteAll(model);
                }
                return success;
        }
index 675835d3a8f97e1fb28d960046de718928d92bf5..64bca82562248e47ef90de1c512b44354defa438 100644 (file)
@@ -543,7 +543,7 @@ public class UserModel implements Principal, Serializable, Comparable<UserModel>
                        // admins can create any repository\r
                        return true;\r
                }\r
-               if (canCreate) {\r
+               if (canCreate()) {\r
                        String projectPath = StringUtils.getFirstPathElement(repository);\r
                        if (!StringUtils.isEmpty(projectPath) && projectPath.equalsIgnoreCase(getPersonalPath())) {\r
                                // personal repository\r
@@ -552,6 +552,16 @@ public class UserModel implements Principal, Serializable, Comparable<UserModel>
                }\r
                return false;\r
        }\r
+       \r
+       /**\r
+        * Returns true if the user is allowed to administer the specified repository\r
+        * \r
+        * @param repo\r
+        * @return true if the user can administer the repository\r
+        */\r
+       public boolean canAdmin(RepositoryModel repo) {\r
+               return canAdmin() || isMyPersonalRepository(repo.name);\r
+       }\r
 \r
        public boolean isAuthenticated() {\r
                return !UserModel.ANONYMOUS.equals(this) && isAuthenticated;\r
index bcf30c26acbcf5753fccedb75ae5dd369a077baf..6809ba629198b9a8fa1084af2d92d6f85a4a098e 100644 (file)
@@ -165,7 +165,7 @@ public class WelcomeShell implements Factory<Command> {
                                msg.append(nl);
                                msg.append(nl);
 
-                               msg.append(String.format("   cat ~/.ssh/id_rsa.pub | ssh -l %s -p %d %s gitblit keys add -", user.username, port, hostname));
+                               msg.append(String.format("   cat ~/.ssh/id_rsa.pub | ssh -l %s -p %d %s gitblit keys add", user.username, port, hostname));
                                msg.append(nl);
                                msg.append(nl);
 
index 56f2c355e105629d392a2f56ab37d8dbd89a9d36..930c058f21ad7f12af83982895e42362775d7670 100644 (file)
@@ -36,7 +36,7 @@ abstract class BaseKeyCommand extends SshCommand {
        protected List<String> readKeys(List<String> sshKeys)
                        throws UnsupportedEncodingException, IOException {
                int idx = -1;
-               if ((idx = sshKeys.indexOf("-")) >= 0) {
+               if (sshKeys.isEmpty() || (idx = sshKeys.indexOf("-")) >= 0) {
                        String sshKey = "";
                        BufferedReader br = new BufferedReader(new InputStreamReader(
                                        in, Charsets.UTF_8));
index 5f508e60342aef887b29f918d727b7560a1632ff..9bb600033ebe894bf4a8cb7da0373b0e09efa313 100644 (file)
@@ -59,7 +59,7 @@ public class KeysDispatcher extends DispatchCommand {
 
                protected final Logger log = LoggerFactory.getLogger(getClass());
 
-               @Argument(metaVar = "-|<KEY>", usage = "the key(s) to add", required = true)
+               @Argument(metaVar = "<KEY>", usage = "the key(s) to add")
                private List<String> addKeys = new ArrayList<String>();
 
                @Override
@@ -82,7 +82,7 @@ public class KeysDispatcher extends DispatchCommand {
 
                private final String ALL = "ALL";
 
-               @Argument(metaVar = "-|<INDEX>|<KEY>|ALL", usage = "the key to remove", required = true)
+               @Argument(metaVar = "<INDEX>|<KEY>|ALL", usage = "the key to remove", required = true)
                private List<String> removeKeys = new ArrayList<String>();
 
                @Override
index f2fbabbe171acebb0c151575c0865d8dc4063379..292c21261cb44279402d7f8ced7764d1b56b0663 100644 (file)
  */
 package com.gitblit.transport.ssh.gitblit;
 
+import java.util.ArrayList;
+import java.util.Collection;
 import java.util.List;
 
+import org.kohsuke.args4j.Argument;
+
+import com.gitblit.GitBlitException;
+import com.gitblit.Keys;
+import com.gitblit.Constants.AccessRestrictionType;
+import com.gitblit.Constants.AuthorizationControl;
 import com.gitblit.manager.IGitblit;
+import com.gitblit.models.RegistrantAccessPermission;
 import com.gitblit.models.RepositoryModel;
 import com.gitblit.models.UserModel;
 import com.gitblit.transport.ssh.commands.CommandMetaData;
 import com.gitblit.transport.ssh.commands.DispatchCommand;
 import com.gitblit.transport.ssh.commands.ListFilterCommand;
+import com.gitblit.transport.ssh.commands.SshCommand;
 import com.gitblit.transport.ssh.commands.UsageExample;
 import com.gitblit.utils.ArrayUtils;
 import com.gitblit.utils.FlipTable;
 import com.gitblit.utils.FlipTable.Borders;
+import com.gitblit.utils.StringUtils;
 import com.google.common.base.Joiner;
 
 @CommandMetaData(name = "repositories", aliases = { "repos" }, description = "Repository management commands")
@@ -34,7 +45,411 @@ public class RepositoriesDispatcher extends DispatchCommand {
 
        @Override
        protected void setup(UserModel user) {
+               // primary commands
+               register(user, NewRepository.class);
+               register(user, RenameRepository.class);
+               register(user, RemoveRepository.class);
+               register(user, ShowRepository.class);
                register(user, ListRepositories.class);
+
+               // repository-specific commands
+               register(user, SetField.class);
+       }
+
+       public static abstract class RepositoryCommand extends SshCommand {
+               @Argument(index = 0, required = true, metaVar = "REPOSITORY", usage = "repository")
+               protected String repository;
+
+               protected RepositoryModel getRepository(boolean requireRepository) throws UnloggedFailure {
+                       IGitblit gitblit = getContext().getGitblit();
+                       RepositoryModel repo = gitblit.getRepositoryModel(repository);
+                       if (requireRepository && repo == null) {
+                               throw new UnloggedFailure(1, String.format("Repository %s does not exist!", repository));
+                       }
+                       return repo;
+               }
+               
+               protected String sanitize(String name) throws UnloggedFailure {
+                       // automatically convert backslashes to forward slashes
+                       name = name.replace('\\', '/');
+                       // Automatically replace // with /
+                       name = name.replace("//", "/");
+
+                       // prohibit folder paths
+                       if (name.startsWith("/")) {
+                               throw new UnloggedFailure(1,  "Illegal leading slash");
+                       }
+                       if (name.startsWith("../")) {
+                               throw new UnloggedFailure(1,  "Illegal relative slash");
+                       }
+                       if (name.contains("/../")) {
+                               throw new UnloggedFailure(1,  "Illegal relative slash");
+                       }
+                       if (name.endsWith("/")) {
+                               name = name.substring(0, name.length() - 1);
+                       }
+                       return name;
+               }
+       }
+
+       @CommandMetaData(name = "new", aliases = { "add" }, description = "Create a new repository")
+       @UsageExample(syntax = "${cmd} myRepo")
+       public static class NewRepository extends RepositoryCommand {
+
+               @Override
+               public void run() throws UnloggedFailure {
+
+                       UserModel user = getContext().getClient().getUser();
+
+                       String name = sanitize(repository);
+                       
+                       if (!user.canCreate(name)) {
+                               // try to prepend personal path
+                               String path  = StringUtils.getFirstPathElement(name);
+                               if ("".equals(path)) {
+                                       name = user.getPersonalPath() + "/" + name;
+                               }
+                       }
+
+                       if (getRepository(false) != null) {
+                               throw new UnloggedFailure(1, String.format("Repository %s already exists!", name));
+                       }
+                                               
+                       if (!user.canCreate(name)) {
+                               throw new UnloggedFailure(1,  String.format("Sorry, you do not have permission to create %s", name));
+                       }
+
+                       IGitblit gitblit = getContext().getGitblit();
+
+                       RepositoryModel repo = new RepositoryModel();
+                       repo.name = name;
+                       repo.projectPath = StringUtils.getFirstPathElement(name);
+                       String restriction = gitblit.getSettings().getString(Keys.git.defaultAccessRestriction, "PUSH");
+                       repo.accessRestriction = AccessRestrictionType.fromName(restriction);
+                       String authorization = gitblit.getSettings().getString(Keys.git.defaultAuthorizationControl, null);
+                       repo.authorizationControl = AuthorizationControl.fromName(authorization);
+
+                       if (user.isMyPersonalRepository(name)) {
+                               // personal repositories are private by default
+                               repo.addOwner(user.username);
+                               repo.accessRestriction = AccessRestrictionType.VIEW;
+                               repo.authorizationControl = AuthorizationControl.NAMED;
+                       }
+
+                       try {
+                               gitblit.updateRepositoryModel(repository,  repo, true);
+                               stdout.println(String.format("%s created.", repo.name));
+                       } catch (GitBlitException e) {
+                               log.error("Failed to add " + repository, e);
+                               throw new UnloggedFailure(1, e.getMessage());
+                       }
+               }
+       }
+
+       @CommandMetaData(name = "rename", aliases = { "mv" }, description = "Rename a repository")
+       @UsageExample(syntax = "${cmd} myRepo.git otherRepo.git", description = "Rename the repository from myRepo.git to otherRepo.git")
+       public static class RenameRepository extends RepositoryCommand {
+               @Argument(index = 1, required = true, metaVar = "NEWNAME", usage = "the new repository name")
+               protected String newRepositoryName;
+
+                               @Override
+               public void run() throws UnloggedFailure {
+                       RepositoryModel repo = getRepository(true);
+                       IGitblit gitblit = getContext().getGitblit();
+                       UserModel user = getContext().getClient().getUser();
+
+                       String name = sanitize(newRepositoryName);
+                       if (!user.canCreate(name)) {
+                               // try to prepend personal path
+                               String path  = StringUtils.getFirstPathElement(name);
+                               if ("".equals(path)) {
+                                       name = user.getPersonalPath() + "/" + name;
+                               }
+                       }
+
+                       if (null != gitblit.getRepositoryModel(name)) {
+                               throw new UnloggedFailure(1, String.format("Repository %s already exists!", name));
+                       }
+
+                       if (repo.name.equalsIgnoreCase(name)) {
+                               throw new UnloggedFailure(1, "Repository names are identical");
+                       }
+                       
+                       if (!user.canAdmin(repo)) {
+                               throw new UnloggedFailure(1,  String.format("Sorry, you do not have permission to rename %s", repository));
+                       }
+                       
+                       if (!user.canCreate(name)) {
+                               throw new UnloggedFailure(1, String.format("Sorry, you don't have permission to move %s to %s/", repository, name));
+                       }
+
+                       // set the new name
+                       repo.name = name;
+
+                       try {
+                               gitblit.updateRepositoryModel(repository, repo, false);
+                               stdout.println(String.format("Renamed repository %s to %s.", repository, name));
+                       } catch (GitBlitException e) {
+                               String msg = String.format("Failed to rename repository from %s to %s", repository, name);
+                               log.error(msg, e);
+                               throw new UnloggedFailure(1, msg);
+                       }
+               }
+       }
+
+       @CommandMetaData(name = "set", description = "Set the specified field of a repository")
+       @UsageExample(syntax = "${cmd} myRepo description John's personal projects", description = "Set the description of a repository")
+       public static class SetField extends RepositoryCommand {
+
+               @Argument(index = 1, required = true, metaVar = "FIELD", usage = "the field to update")
+               protected String fieldName;
+
+               @Argument(index = 2, required = true, metaVar = "VALUE", usage = "the new value")
+               protected List<String> fieldValues = new ArrayList<String>();
+
+               protected enum Field {
+                       description;
+
+                       static Field fromString(String name) {
+                               for (Field field : values()) {
+                                       if (field.name().equalsIgnoreCase(name)) {
+                                               return field;
+                                       }
+                               }
+                               return null;
+                       }
+               }
+
+               @Override
+               protected String getUsageText() {
+                       String fields = Joiner.on(", ").join(Field.values());
+                       StringBuilder sb = new StringBuilder();
+                       sb.append("Valid fields are:\n   ").append(fields);
+                       return sb.toString();
+               }
+
+               @Override
+               public void run() throws UnloggedFailure {
+                       RepositoryModel repo = getRepository(true);
+
+                       Field field = Field.fromString(fieldName);
+                       if (field == null) {
+                               throw new UnloggedFailure(1, String.format("Unknown field %s", fieldName));
+                       }
+
+                       if (!getContext().getClient().getUser().canAdmin(repo)) {
+                               throw new UnloggedFailure(1,  String.format("Sorry, you do not have permission to administer %s", repository));
+                       }
+
+                       String value = Joiner.on(" ").join(fieldValues).trim();
+                       IGitblit gitblit = getContext().getGitblit();
+
+                       switch(field) {
+                       case description:
+                               repo.description = value;
+                               break;
+                       default:
+                               throw new UnloggedFailure(1,  String.format("Field %s was not properly handled by the set command.", fieldName));
+                       }
+
+                       try {
+                               gitblit.updateRepositoryModel(repo.name,  repo, false);
+                               stdout.println(String.format("Set %s.%s = %s", repo.name, fieldName, value));
+                       } catch (GitBlitException e) {
+                               String msg = String.format("Failed to set %s.%s = %s", repo.name, fieldName, value);
+                               log.error(msg, e);
+                               throw new UnloggedFailure(1, msg);
+                       }
+               }
+
+               protected boolean toBool(String value) throws UnloggedFailure {
+                       String v = value.toLowerCase();
+                       if (v.equals("t")
+                                       || v.equals("true")
+                                       || v.equals("yes")
+                                       || v.equals("on")
+                                       || v.equals("y")
+                                       || v.equals("1")) {
+                               return true;
+                       } else if (v.equals("f")
+                                       || v.equals("false")
+                                       || v.equals("no")
+                                       || v.equals("off")
+                                       || v.equals("n")
+                                       || v.equals("0")) {
+                               return false;
+                       }
+                       throw new UnloggedFailure(1,  String.format("Invalid boolean value %s", value));
+               }
+       }
+
+       @CommandMetaData(name = "remove", aliases = { "rm" }, description = "Remove a repository")
+       @UsageExample(syntax = "${cmd} myRepo.git", description = "Delete myRepo.git")
+       public static class RemoveRepository extends RepositoryCommand {
+
+               @Override
+               public void run() throws UnloggedFailure {
+
+                       RepositoryModel repo = getRepository(true);
+                       
+                       if (!getContext().getClient().getUser().canAdmin(repo)) {
+                               throw new UnloggedFailure(1,  String.format("Sorry, you do not have permission to delete %s", repository));
+                       }
+
+                       IGitblit gitblit = getContext().getGitblit();
+                       if (gitblit.deleteRepositoryModel(repo)) {
+                               stdout.println(String.format("%s has been deleted.", repository));
+                       } else {
+                               throw new UnloggedFailure(1, String.format("Failed to delete %s!", repository));
+                       }
+               }
+       }
+
+       @CommandMetaData(name = "show", description = "Show the details of a repository")
+       @UsageExample(syntax = "${cmd} myRepo.git", description = "Display myRepo.git")
+       public static class ShowRepository extends RepositoryCommand {
+
+               @Override
+               public void run() throws UnloggedFailure {
+
+                       RepositoryModel r = getRepository(true);
+
+                       if (!getContext().getClient().getUser().canAdmin(r)) {
+                               throw new UnloggedFailure(1,  String.format("Sorry, you do not have permission to see the %s settings.", repository));
+                       }
+
+                       IGitblit gitblit = getContext().getGitblit();
+
+                       // fields
+                       StringBuilder fb = new StringBuilder();
+                       fb.append("Description    : ").append(toString(r.description)).append('\n');
+                       fb.append("Origin         : ").append(toString(r.origin)).append('\n');
+                       fb.append("Default Branch : ").append(toString(r.HEAD)).append('\n');
+                       fb.append('\n');
+                       fb.append("GC Period    : ").append(r.gcPeriod).append('\n');
+                       fb.append("GC Threshold : ").append(r.gcThreshold).append('\n');
+                       fb.append('\n');
+                       fb.append("Accept Tickets   : ").append(toString(r.acceptNewTickets)).append('\n');
+                       fb.append("Accept Patchsets : ").append(toString(r.acceptNewPatchsets)).append('\n');
+                       fb.append("Require Approval : ").append(toString(r.requireApproval)).append('\n');
+                       fb.append("Merge To         : ").append(toString(r.mergeTo)).append('\n');
+                       fb.append('\n');
+                       fb.append("Incremental push tags    : ").append(toString(r.useIncrementalPushTags)).append('\n');
+                       fb.append("Show remote branches     : ").append(toString(r.showRemoteBranches)).append('\n');
+                       fb.append("Skip size calculations   : ").append(toString(r.skipSizeCalculation)).append('\n');
+                       fb.append("Skip summary metrics     : ").append(toString(r.skipSummaryMetrics)).append('\n');
+                       fb.append("Max activity commits     : ").append(r.maxActivityCommits).append('\n');
+                       fb.append("Author metric exclusions : ").append(toString(r.metricAuthorExclusions)).append('\n');
+                       fb.append("Commit Message Renderer  : ").append(r.commitMessageRenderer).append('\n');
+                       fb.append("Mailing Lists            : ").append(toString(r.mailingLists)).append('\n');
+                       fb.append('\n');
+                       fb.append("Access Restriction    : ").append(r.accessRestriction).append('\n');
+                       fb.append("Authorization Control : ").append(r.authorizationControl).append('\n');
+                       fb.append('\n');
+                       fb.append("Is Frozen        : ").append(toString(r.isFrozen)).append('\n');
+                       fb.append("Allow Forks      : ").append(toString(r.allowForks)).append('\n');
+                       fb.append("Verify Committer : ").append(toString(r.verifyCommitter)).append('\n');
+                       fb.append('\n');
+                       fb.append("Federation Strategy : ").append(r.federationStrategy).append('\n');
+                       fb.append("Federation Sets     : ").append(toString(r.federationSets)).append('\n');
+                       fb.append('\n');
+                       fb.append("Indexed Branches : ").append(toString(r.indexedBranches)).append('\n');
+                       fb.append('\n');
+                       fb.append("Pre-Receive Scripts  : ").append(toString(r.preReceiveScripts)).append('\n');
+                       fb.append("           inherited : ").append(toString(gitblit.getPreReceiveScriptsInherited(r))).append('\n');
+                       fb.append("Post-Receive Scripts : ").append(toString(r.postReceiveScripts)).append('\n');
+                       fb.append("           inherited : ").append(toString(gitblit.getPostReceiveScriptsInherited(r))).append('\n');
+                       String fields = fb.toString();
+
+                       // owners
+                       String owners;
+                       if (r.owners.isEmpty()) {
+                               owners = FlipTable.EMPTY;
+                       } else {
+                               String[] pheaders = { "Account", "Name" };
+                               Object [][] pdata = new Object[r.owners.size()][];
+                               for (int i = 0; i < r.owners.size(); i++) {
+                                       String owner = r.owners.get(i);
+                                       UserModel u = gitblit.getUserModel(owner);
+                                       pdata[i] = new Object[] { owner, u == null ? "" : u.getDisplayName() };
+                               }
+                               owners = FlipTable.of(pheaders, pdata, Borders.COLS);
+                       }
+
+                       // team permissions
+                       List<RegistrantAccessPermission> tperms = gitblit.getTeamAccessPermissions(r);
+                       String tpermissions;
+                       if (tperms.isEmpty()) {
+                               tpermissions = FlipTable.EMPTY;
+                       } else {
+                               String[] pheaders = { "Team", "Permission", "Type" };
+                               Object [][] pdata = new Object[tperms.size()][];
+                               for (int i = 0; i < tperms.size(); i++) {
+                                       RegistrantAccessPermission ap = tperms.get(i);
+                                       pdata[i] = new Object[] { ap.registrant, ap.permission, ap.permissionType };
+                               }
+                               tpermissions = FlipTable.of(pheaders, pdata, Borders.COLS);
+                       }
+
+                       // user permissions
+                       List<RegistrantAccessPermission> uperms = gitblit.getUserAccessPermissions(r);
+                       String upermissions;
+                       if (uperms.isEmpty()) {
+                               upermissions = FlipTable.EMPTY;
+                       } else {
+                               String[] pheaders = { "Account", "Name", "Permission", "Type", "Source", "Mutable" };
+                               Object [][] pdata = new Object[uperms.size()][];
+                               for (int i = 0; i < uperms.size(); i++) {
+                                       RegistrantAccessPermission ap = uperms.get(i);
+                                       String name = "";
+                                       try {
+                                               String dn = gitblit.getUserModel(ap.registrant).displayName;
+                                               if (dn != null) {
+                                                       name = dn;
+                                               }
+                                       } catch (Exception e) {
+                                       }
+                                       pdata[i] = new Object[] { ap.registrant, name, ap.permission, ap.permissionType, ap.source, ap.mutable ? "Y":"" };
+                               }
+                               upermissions = FlipTable.of(pheaders, pdata, Borders.COLS);
+                       }
+
+                       // assemble table
+                       String title = r.name;
+                       String [] headers = new String[] { title };
+                       String[][] data = new String[8][];
+                       data[0] = new String [] { "FIELDS" };
+                       data[1] = new String [] {fields };
+                       data[2] = new String [] { "OWNERS" };
+                       data[3] = new String [] { owners };
+                       data[4] = new String [] { "TEAM PERMISSIONS" };
+                       data[5] = new String [] { tpermissions };
+                       data[6] = new String [] { "USER PERMISSIONS" };
+                       data[7] = new String [] { upermissions };
+                       stdout.println(FlipTable.of(headers, data));
+               }
+               
+               protected String toString(String val) {
+                       if (val == null) {
+                               return "";
+                       }
+                       return val;
+               }
+               
+               protected String toString(Collection<?> collection) {
+                       if (collection == null) {
+                               return "";
+                       }
+                       return Joiner.on(", ").join(collection);
+               }
+               
+               protected String toString(boolean val) {
+                       if (val) {
+                               return "Y";
+                       }
+                       return "";
+               }
+
        }
 
        /* List repositories */
index 5bac2ff49b1bb078cc0eae91b1a31f0d8c5777fc..0f09910e88e8442c1d659be6335a38fcd1469dc2 100644 (file)
@@ -23,8 +23,8 @@ First you'll need to create an SSH key pair, if you don't already have one or if
 \r
 Then you can upload your *public* key right from the command-line.\r
 \r
-    cat ~/.ssh/id_rsa.pub | ssh -l <username> -p 29418 <hostname> gitblit keys add -\r
-    cat c:\<userfolder>\.ssh\id_rsa.pub | ssh -l <username> -p 29418 <hostname> gitblit keys add -\r
+    cat ~/.ssh/id_rsa.pub | ssh -l <username> -p 29418 <hostname> gitblit keys add\r
+    cat c:\<userfolder>\.ssh\id_rsa.pub | ssh -l <username> -p 29418 <hostname> gitblit keys add\r
 \r
 **NOTE:** It is important to note that *ssh-keygen* generates a public/private keypair (e.g. id_rsa and id_rsa.pub).  You want to upload the *public* key, which is denoted by the *.pub* file extension.\r
 \r
@@ -62,7 +62,7 @@ The *gitblit* command has many subcommands for interacting with Gitblit.
 \r
 Add an SSH public key to your account.  This command accepts a public key piped to stdin.\r
 \r
-    cat ~/.ssh/id_rsa.pub | ssh -l <username> -p 29418 <hostname> gitblit keys add -\r
+    cat ~/.ssh/id_rsa.pub | ssh -l <username> -p 29418 <hostname> gitblit keys add\r
 \r
 ##### keys list\r
 \r