From a9dc74e73eea068b8cbb5c96958abccae88b4abc Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Thu, 10 Apr 2014 19:00:52 -0400
Subject: [PATCH] Implement management commands in repositories dispatcher

---
 src/main/java/com/gitblit/models/UserModel.java                             |   12 +
 src/site/setup_transport_ssh.mkd                                            |    6 
 src/main/java/com/gitblit/transport/ssh/gitblit/BaseKeyCommand.java         |    2 
 src/main/java/com/gitblit/transport/ssh/gitblit/RepositoriesDispatcher.java |  415 +++++++++++++++++++++++++++++++++++++++++++++++++++
 src/main/java/com/gitblit/transport/ssh/gitblit/KeysDispatcher.java         |    4 
 src/main/java/com/gitblit/GitBlit.java                                      |    2 
 src/main/java/com/gitblit/transport/ssh/WelcomeShell.java                   |    2 
 7 files changed, 434 insertions(+), 9 deletions(-)

diff --git a/src/main/java/com/gitblit/GitBlit.java b/src/main/java/com/gitblit/GitBlit.java
index 430ae6d..26ab3f3 100644
--- a/src/main/java/com/gitblit/GitBlit.java
+++ b/src/main/java/com/gitblit/GitBlit.java
@@ -226,7 +226,7 @@
 	public boolean deleteRepositoryModel(RepositoryModel model) {
 		boolean success = repositoryManager.deleteRepositoryModel(model);
 		if (success && ticketService != null) {
-			return ticketService.deleteAll(model);
+			ticketService.deleteAll(model);
 		}
 		return success;
 	}
diff --git a/src/main/java/com/gitblit/models/UserModel.java b/src/main/java/com/gitblit/models/UserModel.java
index 675835d..64bca82 100644
--- a/src/main/java/com/gitblit/models/UserModel.java
+++ b/src/main/java/com/gitblit/models/UserModel.java
@@ -543,7 +543,7 @@
 			// admins can create any repository
 			return true;
 		}
-		if (canCreate) {
+		if (canCreate()) {
 			String projectPath = StringUtils.getFirstPathElement(repository);
 			if (!StringUtils.isEmpty(projectPath) && projectPath.equalsIgnoreCase(getPersonalPath())) {
 				// personal repository
@@ -552,6 +552,16 @@
 		}
 		return false;
 	}
+	
+	/**
+	 * Returns true if the user is allowed to administer the specified repository
+	 * 
+	 * @param repo
+	 * @return true if the user can administer the repository
+	 */
+	public boolean canAdmin(RepositoryModel repo) {
+		return canAdmin() || isMyPersonalRepository(repo.name);
+	}
 
 	public boolean isAuthenticated() {
 		return !UserModel.ANONYMOUS.equals(this) && isAuthenticated;
diff --git a/src/main/java/com/gitblit/transport/ssh/WelcomeShell.java b/src/main/java/com/gitblit/transport/ssh/WelcomeShell.java
index bcf30c2..6809ba6 100644
--- a/src/main/java/com/gitblit/transport/ssh/WelcomeShell.java
+++ b/src/main/java/com/gitblit/transport/ssh/WelcomeShell.java
@@ -165,7 +165,7 @@
 				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);
 
diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/BaseKeyCommand.java b/src/main/java/com/gitblit/transport/ssh/gitblit/BaseKeyCommand.java
index 56f2c35..930c058 100644
--- a/src/main/java/com/gitblit/transport/ssh/gitblit/BaseKeyCommand.java
+++ b/src/main/java/com/gitblit/transport/ssh/gitblit/BaseKeyCommand.java
@@ -36,7 +36,7 @@
 	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));
diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/KeysDispatcher.java b/src/main/java/com/gitblit/transport/ssh/gitblit/KeysDispatcher.java
index 5f508e6..9bb6000 100644
--- a/src/main/java/com/gitblit/transport/ssh/gitblit/KeysDispatcher.java
+++ b/src/main/java/com/gitblit/transport/ssh/gitblit/KeysDispatcher.java
@@ -59,7 +59,7 @@
 
 		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 @@
 
 		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
diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/RepositoriesDispatcher.java b/src/main/java/com/gitblit/transport/ssh/gitblit/RepositoriesDispatcher.java
index f2fbabb..292c212 100644
--- a/src/main/java/com/gitblit/transport/ssh/gitblit/RepositoriesDispatcher.java
+++ b/src/main/java/com/gitblit/transport/ssh/gitblit/RepositoriesDispatcher.java
@@ -15,18 +15,29 @@
  */
 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 @@
 
 	@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 */
diff --git a/src/site/setup_transport_ssh.mkd b/src/site/setup_transport_ssh.mkd
index 5bac2ff..0f09910 100644
--- a/src/site/setup_transport_ssh.mkd
+++ b/src/site/setup_transport_ssh.mkd
@@ -23,8 +23,8 @@
 
 Then you can upload your *public* key right from the command-line.
 
-    cat ~/.ssh/id_rsa.pub | ssh -l <username> -p 29418 <hostname> gitblit keys add -
-    cat c:\<userfolder>\.ssh\id_rsa.pub | ssh -l <username> -p 29418 <hostname> gitblit keys add -
+    cat ~/.ssh/id_rsa.pub | ssh -l <username> -p 29418 <hostname> gitblit keys add
+    cat c:\<userfolder>\.ssh\id_rsa.pub | ssh -l <username> -p 29418 <hostname> gitblit keys add
 
 **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.
 
@@ -62,7 +62,7 @@
 
 Add an SSH public key to your account.  This command accepts a public key piped to stdin.
 
-    cat ~/.ssh/id_rsa.pub | ssh -l <username> -p 29418 <hostname> gitblit keys add -
+    cat ~/.ssh/id_rsa.pub | ssh -l <username> -p 29418 <hostname> gitblit keys add
 
 ##### keys list
 

--
Gitblit v1.9.1