Sebastian Sdorra avatar Sebastian Sdorra committed 0517fff Merge

merge with branch issue-339

Comments (0)

Files changed (3)

scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitFileHook.java

+/**
+ * Copyright (c) 2010, Sebastian Sdorra
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 3. Neither the name of SCM-Manager; nor the names of its
+ *    contributors may be used to endorse or promote products derived from this
+ *    software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * http://bitbucket.org/sdorra/scm-manager
+ *
+ */
+
+
+package sonia.scm.web;
+
+//~--- non-JDK imports --------------------------------------------------------
+
+import com.google.common.base.Stopwatch;
+import com.google.common.io.Closeables;
+
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.transport.ReceivePack;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import sonia.scm.repository.GitUtil;
+import sonia.scm.repository.RepositoryHookType;
+import sonia.scm.util.IOUtil;
+
+//~--- JDK imports ------------------------------------------------------------
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.PrintWriter;
+
+
+/**
+ *
+ * @author Sebastian Sdorra
+ */
+public class GitFileHook
+{
+
+  /** Field description */
+  public static final String FILE_HOOKDIRECTORY = "hooks";
+
+  /** Field description */
+  public static final String FILE_HOOK_POST_RECEIVE = "post-receive";
+
+  /** Field description */
+  public static final String FILE_HOOK_PRE_RECEIVE = "pre-receive";
+
+  /**
+   * the logger for GitFileHook
+   */
+  private static final Logger logger =
+    LoggerFactory.getLogger(GitFileHook.class);
+
+  //~--- constructors ---------------------------------------------------------
+
+  /**
+   * Constructs ...
+   *
+   *
+   *
+   *
+   * @param executor
+   * @param type
+   * @param rpack
+   * @param commands
+   */
+  private GitFileHook(RepositoryHookType type,
+    ReceivePack rpack, Iterable<ReceiveCommand> commands)
+  {
+    this.type = type;
+    this.rpack = rpack;
+    this.commands = commands;
+  }
+
+  //~--- methods --------------------------------------------------------------
+
+  /**
+   * Method description
+   *
+   *
+   *
+   * @param executor
+   * @param type
+   * @param rpack
+   * @param commands
+   */
+  public static void execute(RepositoryHookType type,
+    ReceivePack rpack, Iterable<ReceiveCommand> commands)
+  {
+    new GitFileHook(type, rpack, commands).execute();
+  }
+
+  /**
+   * Method description
+   *
+   *
+   * @param hook
+   *
+   * @return
+   *
+   * @throws IOException
+   */
+  private Process createProcess(File hook) throws IOException
+  {
+    ProcessBuilder pb = new ProcessBuilder(hook.getAbsolutePath());
+
+    // use repostitory directory as working directory for file hooks
+    // see issue #99
+    pb.directory(rpack.getRepository().getDirectory());
+
+    // copy system environment for hook
+    pb.environment().putAll(System.getenv());
+
+    // start process
+    return pb.redirectErrorStream(true).start();
+  }
+
+  /**
+   * Method description
+   *
+   *
+   * @param rc
+   *
+   * @return
+   */
+  private String createReceiveCommandOutput(ReceiveCommand rc)
+  {
+    StringBuilder sb = new StringBuilder();
+
+    sb.append(GitUtil.getId(rc.getOldId()));
+    sb.append(" ");
+    sb.append(GitUtil.getId(rc.getNewId()));
+    sb.append(" ");
+    sb.append(rc.getRefName());
+
+    return sb.toString();
+  }
+
+  /**
+   * Method description
+   *
+   *
+   * @param type
+   */
+  private void execute()
+  {
+    File hook = getHookFile();
+
+    if ((hook == null) ||!hook.exists())
+    {
+      logger.trace("no file hook found for {}", type);
+    }
+    else if (!hook.canExecute())
+    {
+      logger.warn("hook file {} is not executeable", hook);
+    }
+    else
+    {
+      if (logger.isDebugEnabled())
+      {
+        logger.debug("try to execute hook {}", hook);
+
+        Stopwatch sw = new Stopwatch().start();
+
+        execute(hook);
+        logger.debug("file hook {} executed in {}", hook, sw.stop());
+      }
+      else
+      {
+        execute(hook);
+      }
+    }
+  }
+
+  /**
+   * Method description
+   *
+   *
+   * @param hook
+   */
+  private void execute(File hook)
+  {
+    Process p;
+    PrintWriter writer = null;
+    BufferedReader stdReader = null;
+
+    try
+    {
+      p = createProcess(hook);
+      writer = new PrintWriter(p.getOutputStream());
+      stdReader = new BufferedReader(new InputStreamReader(p.getInputStream()));
+
+      for (ReceiveCommand rc : commands)
+      {
+        String output = createReceiveCommandOutput(rc);
+
+        logger.trace("write rc output \"{}\" to hook {}", output, hook);
+        writer.println(output);
+      }
+      
+      writer.close();
+      
+      String line = stdReader.readLine();
+
+      while (line != null)
+      {
+        rpack.sendMessage(line);
+        line = stdReader.readLine();
+      }
+
+      // TODO handle timeout
+      int result = p.waitFor();
+
+      if (result == 0)
+      {
+        logger.debug("file hook {} executed successful", hook);
+      }
+      else
+      {
+        logger.warn("file hook {} returned with exit code {}", hook, result);
+        GitHooks.abortIfPossible(type, rpack, commands);
+      }
+    }
+    catch (Exception ex)
+    {
+      logger.error("failure during file hook execution", ex);
+      GitHooks.abortIfPossible(type, rpack, commands,
+        "failure during file hook execution");
+    }
+    finally
+    {
+      Closeables.closeQuietly(writer);
+      Closeables.closeQuietly(stdReader);
+    }
+  }
+
+  //~--- get methods ----------------------------------------------------------
+
+  /**
+   * Method description
+   *
+   *
+   * @param type
+   *
+   * @return
+   */
+  private File getHookFile()
+  {
+    File hook = null;
+    File directory = rpack.getRepository().getDirectory();
+    String scriptName = null;
+
+    if (type == RepositoryHookType.POST_RECEIVE)
+    {
+      scriptName = FILE_HOOK_POST_RECEIVE;
+    }
+    else if (type == RepositoryHookType.PRE_RECEIVE)
+    {
+      scriptName = FILE_HOOK_PRE_RECEIVE;
+    }
+
+    if (scriptName != null)
+    {
+      hook = getHookFile(directory, scriptName);
+    }
+
+    return hook;
+  }
+
+  /**
+   * Method description
+   *
+   *
+   * @param directory
+   * @param name
+   *
+   * @return
+   */
+  private File getHookFile(File directory, String name)
+  {
+    //J-
+    return IOUtil.getScript(
+      new File(
+        directory,
+        FILE_HOOKDIRECTORY.concat(File.separator).concat(name)
+      )
+    );
+    //J+
+  }
+
+  //~--- fields ---------------------------------------------------------------
+
+  /** Field description */
+  private Iterable<ReceiveCommand> commands;
+
+  /** Field description */
+  private ReceivePack rpack;
+
+  /** Field description */
+  private RepositoryHookType type;
+}

scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitHooks.java

+/**
+ * Copyright (c) 2010, Sebastian Sdorra
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 3. Neither the name of SCM-Manager; nor the names of its
+ *    contributors may be used to endorse or promote products derived from this
+ *    software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * http://bitbucket.org/sdorra/scm-manager
+ *
+ */
+
+
+package sonia.scm.web;
+
+//~--- non-JDK imports --------------------------------------------------------
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.transport.ReceivePack;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import sonia.scm.repository.RepositoryHookType;
+
+//~--- JDK imports ------------------------------------------------------------
+
+import java.util.List;
+
+/**
+ *
+ * @author Sebastian Sdorra
+ */
+public final class GitHooks
+{
+
+  /** Field description */
+  public static final String PREFIX_MSG = "[SCM] ";
+
+  /**
+   * the logger for GitHooks
+   */
+  private static final Logger logger = LoggerFactory.getLogger(GitHooks.class);
+
+  //~--- methods --------------------------------------------------------------
+
+  /**
+   * Method description
+   *
+   *
+   * @param type
+   * @param rpack
+   * @param commands
+   */
+  public static void abortIfPossible(RepositoryHookType type,
+    ReceivePack rpack, Iterable<ReceiveCommand> commands)
+  {
+    abortIfPossible(type, rpack, commands, null);
+  }
+
+  /**
+   * Method description
+   *
+   *
+   * @param type
+   * @param rpack
+   * @param commands
+   * @param message
+   */
+  public static void abortIfPossible(RepositoryHookType type,
+    ReceivePack rpack, Iterable<ReceiveCommand> commands, String message)
+  {
+    if (type == RepositoryHookType.PRE_RECEIVE)
+    {
+      for (ReceiveCommand rc : commands)
+      {
+        rc.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON);
+      }
+    }
+
+    if (message != null)
+    {
+      sendPrefixedError(rpack, message);
+    }
+  }
+
+  /**
+   * Method description
+   *
+   *
+   * @param type
+   * @param commands
+   *
+   * @return
+   */
+  public static List<ReceiveCommand> filterReceiveable(RepositoryHookType type,
+    Iterable<ReceiveCommand> commands)
+  {
+    List<ReceiveCommand> receiveable = Lists.newArrayList();
+
+    for (ReceiveCommand command : commands)
+    {
+      if (isReceiveable(type, command))
+      {
+        receiveable.add(command);
+      }
+      else
+      {
+        logger.debug("skip receive command, type={}, ref={}, result={}",
+          command.getType(), command.getRefName(), command.getResult());
+      }
+    }
+
+    return receiveable;
+  }
+
+  /**
+   * Method description
+   *
+   *
+   * @param rpack
+   * @param message
+   */
+  public static void sendPrefixedError(ReceivePack rpack, String message)
+  {
+    rpack.sendError(createPrefixedMessage(message));
+  }
+
+  /**
+   * Method description
+   *
+   *
+   * @param rpack
+   * @param message
+   */
+  public static void sendPrefixedMessage(ReceivePack rpack, String message)
+  {
+    rpack.sendMessage(createPrefixedMessage(message));
+  }
+
+  /**
+   * Method description
+   *
+   *
+   * @param message
+   *
+   * @return
+   */
+  private static String createPrefixedMessage(String message)
+  {
+    return PREFIX_MSG.concat(Strings.nullToEmpty(message));
+  }
+
+  //~--- get methods ----------------------------------------------------------
+
+  /**
+   * Method description
+   *
+   *
+   * @param rc
+   * @param type
+   *
+   * @return
+   */
+  private static boolean isReceiveable(RepositoryHookType type,
+    ReceiveCommand rc)
+  {
+    //J-
+    return ((RepositoryHookType.PRE_RECEIVE == type) && 
+            (rc.getResult() == ReceiveCommand.Result.NOT_ATTEMPTED)) || 
+           ((RepositoryHookType.POST_RECEIVE == type) && 
+            (rc.getResult() == ReceiveCommand.Result.OK));
+    //J+
+  }
+}

scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitReceiveHook.java

 
 //~--- non-JDK imports --------------------------------------------------------
 
-import com.google.common.collect.Lists;
-
-import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.transport.PostReceiveHook;
 import org.eclipse.jgit.transport.PreReceiveHook;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import sonia.scm.io.Command;
-import sonia.scm.io.CommandResult;
-import sonia.scm.io.SimpleCommand;
 import sonia.scm.repository.GitRepositoryHandler;
 import sonia.scm.repository.GitRepositoryHookEvent;
-import sonia.scm.repository.GitUtil;
 import sonia.scm.repository.RepositoryHookType;
 import sonia.scm.repository.RepositoryManager;
 import sonia.scm.repository.RepositoryUtil;
-import sonia.scm.util.IOUtil;
-import sonia.scm.util.Util;
 
 //~--- JDK imports ------------------------------------------------------------
 
-import java.io.File;
-import java.io.IOException;
-
 import java.util.Collection;
 import java.util.List;
 
 public class GitReceiveHook implements PreReceiveHook, PostReceiveHook
 {
 
-  /** Field description */
-  public static final String FILE_HOOKDIRECTORY = "hooks";
-
-  /** Field description */
-  public static final String FILE_HOOK_POST_RECEIVE = "post-receive";
-
-  /** Field description */
-  public static final String FILE_HOOK_PRE_RECEIVE = "pre-receive";
-
-  /** Field description */
-  public static final String PREFIX_MSG = "[SCM] ";
-
   /** the logger for GitReceiveHook */
   private static final Logger logger =
     LoggerFactory.getLogger(GitReceiveHook.class);
   /**
    * Method description
    *
-   * @param rpack
-   * @param rc
-   * @param repositoryDirectory
-   * @param hook
-   * @param oldId
-   * @param newId
-   * @param refName
-   */
-  private void executeFileHook(ReceivePack rpack, ReceiveCommand rc,
-    File repositoryDirectory, File hook, ObjectId oldId, ObjectId newId,
-    String refName)
-  {
-    if (logger.isDebugEnabled())
-    {
-      logger.debug("execute file hook '{}' in directoy '{}'");
-    }
-
-    final Command cmd = new SimpleCommand(hook.getAbsolutePath(),
-                          GitUtil.getId(oldId), GitUtil.getId(newId),
-                          Util.nonNull(refName));
-
-    // issue-99
-    cmd.setWorkDirectory(repositoryDirectory);
-
-    try
-    {
-      CommandResult result = cmd.execute();
-
-      if (result.isSuccessfull())
-      {
-        if (logger.isDebugEnabled())
-        {
-          logger.debug("executed file hook successfull");
-
-          if (logger.isTraceEnabled())
-          {
-            String out = result.getOutput();
-
-            if (Util.isNotEmpty(out))
-            {
-              logger.trace(out);
-            }
-          }
-        }
-      }
-      else
-      {
-        if (logger.isErrorEnabled())
-        {
-          logger.error("failed to execute file hook");
-        }
-
-        String out = result.getOutput();
-
-        if (Util.isNotEmpty(out))
-        {
-          logger.error(out);
-        }
-
-        sendError(rpack, rc, out);
-      }
-    }
-    catch (IOException ex)
-    {
-      logger.error("could not execute file hook", ex);
-    }
-  }
-
-  /**
-   * Method description
-   *
-   *
-   * @param rpack
-   * @param rc
-   * @param newId
-   * @param type
-   */
-  private void handleFileHooks(ReceivePack rpack, ReceiveCommand rc,
-    RepositoryHookType type)
-  {
-    ObjectId newId = rc.getNewId();
-    ObjectId oldId = null;
-
-    if (isUpdateCommand(rc))
-    {
-      oldId = rc.getOldId();
-
-      if (logger.isTraceEnabled())
-      {
-        logger.trace("handle update receive command from commit '{}' to '{}'",
-          oldId.getName(), newId.getName());
-      }
-    }
-    else if (logger.isTraceEnabled())
-    {
-      logger.trace("handle receive command for commit '{}'", newId.getName());
-    }
-
-    File directory = rpack.getRepository().getDirectory();
-    String scriptName = null;
-
-    if (type == RepositoryHookType.POST_RECEIVE)
-    {
-      scriptName = FILE_HOOK_POST_RECEIVE;
-    }
-    else if (type == RepositoryHookType.PRE_RECEIVE)
-    {
-      scriptName = FILE_HOOK_PRE_RECEIVE;
-    }
-
-    if (scriptName != null)
-    {
-      File hookScript = getHookScript(directory, scriptName);
-
-      if (hookScript != null)
-      {
-        executeFileHook(rpack, rc, directory, hookScript, oldId, newId,
-          rc.getRefName());
-      }
-    }
-  }
-
-  /**
-   * Method description
-   *
    *
    * @param rpack
    * @param receiveCommands
     {
       logger.error("could not handle receive commands", ex);
 
-      if (type == RepositoryHookType.PRE_RECEIVE)
-      {
-        sendError(rpack, receiveCommands, ex.getMessage());
-      }
+      GitHooks.abortIfPossible(type, rpack, receiveCommands, ex.getMessage());
     }
   }
 
    *
    * @param rpack
    * @param receiveCommands
+   * @param commands
    * @param type
    */
   private void onReceive(ReceivePack rpack,
-    Collection<ReceiveCommand> receiveCommands, RepositoryHookType type)
+    Collection<ReceiveCommand> commands, RepositoryHookType type)
   {
     if (logger.isTraceEnabled())
     {
       logger.trace("received git hook, type={}", type);
     }
 
-    List<ReceiveCommand> commands = Lists.newArrayList();
+    List<ReceiveCommand> receiveCommands = GitHooks.filterReceiveable(type,
+                                             commands);
 
-    for (ReceiveCommand rc : receiveCommands)
+    GitFileHook.execute(type, rpack, commands);
+
+    if (!receiveCommands.isEmpty())
     {
-      if (isReceiveable(rc, type))
-      {
-        commands.add(rc);
-        handleFileHooks(rpack, rc, type);
-      }
-      else if (logger.isTraceEnabled())
-      {
-        //J-
-        logger.trace("skip receive command, type={}, ref={}, result={}",
-          new Object[] { 
-            rc.getType(),
-            rc.getRefName(), 
-            rc.getResult() 
-          }
-        );
-        //J+
-      }
-    }
-
-    if (!commands.isEmpty())
-    {
-      handleReceiveCommands(rpack, commands, type);
+      handleReceiveCommands(rpack, receiveCommands, type);
     }
     else if (logger.isDebugEnabled())
     {
-      logger.debug("no receive command found to process");
+      logger.debug("no receive commands found to process");
     }
   }
 
-  /**
-   * Method description
-   *
-   *
-   * @param rpack
-   * @param commands
-   * @param message
-   */
-  private void sendError(ReceivePack rpack, Iterable<ReceiveCommand> commands,
-    String message)
-  {
-    if (logger.isWarnEnabled())
-    {
-      logger.warn("abort git push request with msg: {}", message);
-    }
-
-    for (ReceiveCommand rc : commands)
-    {
-      rc.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON);
-    }
-
-    rpack.sendError(PREFIX_MSG.concat(Util.nonNull(message)));
-  }
-
-  /**
-   * Method description
-   *
-   *
-   *
-   * @param rpack
-   * @param rc
-   * @param message
-   */
-  private void sendError(ReceivePack rpack, ReceiveCommand rc, String message)
-  {
-    rc.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON);
-    rpack.sendError(PREFIX_MSG.concat(Util.nonNull(message)));
-  }
-
-  //~--- get methods ----------------------------------------------------------
-
-  /**
-   * Method description
-   *
-   *
-   * @param directory
-   * @param name
-   *
-   * @return
-   */
-  private File getHookScript(File directory, String name)
-  {
-    File baseFile = new File(directory,
-                      FILE_HOOKDIRECTORY.concat(File.separator).concat(name));
-
-    return IOUtil.getScript(baseFile);
-  }
-
-  /**
-   * Method description
-   *
-   *
-   * @param rc
-   * @param type
-   *
-   * @return
-   */
-  private boolean isReceiveable(ReceiveCommand rc, RepositoryHookType type)
-  {
-    //J-
-    return ((RepositoryHookType.PRE_RECEIVE == type) && 
-            (rc.getResult() == ReceiveCommand.Result.NOT_ATTEMPTED)) || 
-           ((RepositoryHookType.POST_RECEIVE == type) && 
-            (rc.getResult() == ReceiveCommand.Result.OK));
-    //J+
-  }
-
-  /**
-   * Method description
-   *
-   *
-   * @param rc
-   *
-   * @return
-   */
-  private boolean isUpdateCommand(ReceiveCommand rc)
-  {
-    return (rc.getType() == ReceiveCommand.Type.UPDATE)
-      || (rc.getType() == ReceiveCommand.Type.UPDATE_NONFASTFORWARD);
-  }
-
   //~--- fields ---------------------------------------------------------------
 
   /** Field description */
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.