GitFOSS
feat(git_ssh): make ssh clone/pull/push work again
+ 368
- 89
@@ -1,4 +1,4 @@
-FROM node:alpine as builder
+FROM node:alpine AS builder
 
 ENV NODE_ENV=development
 ENV NODE_OPTIONS=--openssl-legacy-provider

...
@@ -52,7 +52,7 @@ RUN yarn bundle:islands    # Bundle Islands (react-monolith) to ESM/CJS/UMD
 COPY ./public              /usr/src/app/public
 COPY ./app.manifest.json   /usr/src/app/app.manifest.json
 
-FROM node:slim as base
+FROM node:slim AS base
 
 ENV NODE_ENV=production
 ENV NODE_OPTIONS=--openssl-legacy-provider

...
@@ -153,9 +153,14 @@ RUN chmod +x git-shell-commands/no-interactive-login
 # Add ssh command to force client command
 COPY ./data/ssh_command /usr/bin/
 COPY ./data/ssh_command_node /usr/bin/
+COPY ./data/http_client.js /usr/bin/
 RUN chmod +x /usr/bin/ssh_command
 RUN chmod +x /usr/bin/ssh_command_node
 
+RUN touch /opt/ssh_commands.log
+RUN chown git:git /opt/ssh_commands.log
+RUN chmod 644 /opt/ssh_commands.log
+
 # Setup ssh folder and keys
 RUN mkdir -p .ssh
 RUN chmod 700 .ssh

app/controllers/ssh-auth.ts
@@ -7,7 +7,7 @@ import { makeGitServerService } from "../services/gitServer";
 
 const onSSHAuth: ReqHandler<AppRouteParams, AppRoute.SSH_AUTH> = async (
   request,
-  reply
+  reply,
 ) => {
   const gitService = makeGitServerService({
     request,

...
@@ -19,10 +19,14 @@ const onSSHAuth: ReqHandler<AppRouteParams, AppRoute.SSH_AUTH> = async (
 
   const { command, repoSlug, username, publicKey } = request.body;
 
+  console.log("SSH auth request received with body:", request.body);
+
   const result = await gitService.repositoryResolver(
-    repoSlug.replace(/\.git$/, "")
+    repoSlug.replace(/\.git$/, ""),
   );
 
+  console.log("result from repositoryResolver:", result);
+
   let { authMode, gitRepositoryDir } = result;
   gitRepositoryDir = gitRepositoryDir.toString().endsWith(".git")
     ? gitRepositoryDir

...
@@ -33,6 +37,12 @@ const onSSHAuth: ReqHandler<AppRouteParams, AppRoute.SSH_AUTH> = async (
     (authMode === GitServer.AuthMode.PUSH_ONLY &&
       command !== "git-receive-pack") // push
   ) {
+    console.log("Successful auth:", {
+      authMode,
+      command,
+      gitRepositoryDir,
+    });
+
     reply.status(200).send({
       success: true,
       authMode,

...
@@ -47,9 +57,11 @@ const onSSHAuth: ReqHandler<AppRouteParams, AppRoute.SSH_AUTH> = async (
     {
       username,
       password: publicKey,
-    }
+    },
   );
 
+  console.log("is auth valid?", isAuthorizationValid);
+
   if (isAuthorizationValid) {
     const [orgSlug, repoName] = repoSlug.replace(/\.git$/, "").split("/");
     request.prisma.repository

app/islands/RepositoryTreeView.tsx
@@ -107,13 +107,14 @@ const RepositoryTreeView: ReactIsland<
               title={"Branch"}
               onChange={(e) => {
                 console.log("branch changed to: ", e.currentTarget.value);
-                window.__router.push(
-                  buildRouteLink(AppRoute.REPOSITORY_BROWSER_WITH_PATH, {
+                window.location.href = buildRouteLink(
+                  AppRoute.REPOSITORY_BROWSER_WITH_PATH,
+                  {
                     orgSlug: orgSlug,
                     repoSlug: repoSlug,
                     currentRef: e.currentTarget.value,
                     "*": currentPath === "/" ? "" : currentPath,
-                  }),
+                  },
                 );
               }}
               style={{

app/services/gitServer/authorizationResolver.ts
@@ -18,6 +18,12 @@ const makeAuthorizationResolver: ServiceMethodFactory<
 
     const repoSlug = repoSlugUnsafe.replace(/\.(git|pub)$/, "");
 
+    console.log("AuthorizationResolver called with:", {
+      repoPath,
+      username,
+      isPubKeyAuth,
+    });
+
     const user = await request.prisma.user.findUnique({
       where: {
         username,

app/views/repository/RepositoryBrowserView.tsx
@@ -105,13 +105,14 @@ const RepositoryBrowserView: ReactView<RepositoryBrowserViewProps> = ({
                 title={"Branch"}
                 onChange={(e) => {
                   console.log("branch changed to: ", e.currentTarget.value);
-                  window.__router.push(
-                    buildRouteLink(AppRoute.REPOSITORY_BROWSER_WITH_PATH, {
+                  window.location.href = buildRouteLink(
+                    AppRoute.REPOSITORY_BROWSER_WITH_PATH,
+                    {
                       orgSlug: parentOrg.slug,
                       repoSlug: repo.slug,
                       currentRef: e.currentTarget.value,
                       "*": path,
-                    }),
+                    },
                   );
                 }}
                 style={{

new file
data/http_client.d.ts
@@ -0,0 +1,38 @@
+declare module "./http_client" {
+  import { IncomingMessage } from "http";
+
+  export class HttpResponse {
+    readonly statusCode: number;
+    readonly statusText: string;
+    readonly ok: boolean;
+    readonly headers: IncomingMessage["headers"];
+
+    constructor(incoming: IncomingMessage);
+
+    text(): Promise<string>;
+    isJson(): Promise<boolean>;
+    json(): Promise<any>;
+  }
+
+  export interface RequestConfig {
+    headers?: Record<string, string>;
+    body?: string | Buffer | any;
+  }
+
+  export class HttpClient {
+    constructor();
+
+    get(url: string, config?: RequestConfig): Promise<HttpResponse>;
+    post(url: string, config?: RequestConfig): Promise<HttpResponse>;
+    put(url: string, config?: RequestConfig): Promise<HttpResponse>;
+    patch(url: string, config?: RequestConfig): Promise<HttpResponse>;
+    delete(url: string, config?: RequestConfig): Promise<HttpResponse>;
+    head(url: string, config?: RequestConfig): Promise<HttpResponse>;
+    options(url: string, config?: RequestConfig): Promise<HttpResponse>;
+    custom(
+      method: string,
+      url: string,
+      config?: RequestConfig,
+    ): Promise<HttpResponse>;
+  }
+}

new file
data/http_client.js
@@ -0,0 +1,204 @@
+/**
+ * Minimal HTTP client using only Node.js built-ins.
+ *
+ * Exports:
+ *   - HttpClient: class with methods get/post/put/patch/delete/head/options/custom
+ *   - HttpResponse: response wrapper (left unchanged per request)
+ *
+ * HttpResponse API:
+ *   - statusCode: number
+ *   - statusText: string
+ *   - ok: boolean (2xx)
+ *   - headers: object
+ *   - text(): Promise<string>
+ *   - isJson(): Promise<boolean>
+ *   - json(): Promise<any>
+ *
+ * HttpClient usage (example):
+ *   const { HttpClient } = require('./client');
+ *   const client = new HttpClient();
+ *   const res = await client.get('http://localhost:3000/auth', {
+ *     headers: { 'content-type':'application/json' },
+ *     // body: ..., // .post/.put/.patch only
+ *   });
+ *   if (!res.ok) {
+ *     const txt = await res.text();
+ *     return txt;
+ *   }
+ *   if (!await res.isJson()) {
+ *     const txt = await res.text();
+ *     return txt;
+ *   }
+ *   const obj = await res.json();
+ *   // ... whatever with obj ...
+ */
+
+const { request } = require("http");
+const { URL } = require("url");
+
+class HttpResponse {
+  /**
+   * Wrap an IncomingMessage.
+   * @param {import('http').IncomingMessage} incoming
+   */
+  constructor(incoming) {
+    this._incoming = incoming;
+    this._buf = null;
+    this.statusCode = incoming.statusCode || 0;
+    this.statusText = incoming.statusMessage || "";
+    this.ok = this.statusCode >= 200 && this.statusCode < 300;
+    this.headers = incoming.headers;
+    this.header = incoming.header;
+    this._json = null;
+  }
+
+  /**
+   * Buffer the full response body (private).
+   * @returns {Promise<Buffer>}
+   */
+  async #readAll() {
+    return new Promise((resolve, reject) => {
+      const bufs = [];
+      this._incoming.on("data", (b) => bufs.push(b));
+      this._incoming.on("end", () => resolve(Buffer.concat(bufs)));
+      this._incoming.on("error", reject);
+    });
+  }
+
+  /**
+   * Return the response body as a string. Buffers on first call and reuses the value.
+   * @returns {Promise<string>}
+   */
+  async text() {
+    if (!this._buf) this._buf = await this.#readAll();
+    return this._buf.toString();
+  }
+
+  /**
+   * Feature-detect whether the body is valid JSON.
+   * Uses text() as the single source-of-truth for the buffered body.
+   * @returns {Promise<boolean>}
+   */
+  async isJson() {
+    try {
+      this._json = JSON.parse(await this.text());
+      return true;
+    } catch {
+      return false;
+    }
+  }
+
+  /**
+   * Parse and return JSON body. Throws if body is not JSON.
+   * @returns {Promise<any>}
+   */
+  async json() {
+    if ((await this.isJson()) === false) throw new Error("not json");
+    if (this._json == null) this._json = JSON.parse(await this.text());
+    return this._json;
+  }
+}
+
+class HttpClient {
+  /**
+   * Internal request factory that performs a request and returns HttpResponse.
+   * Private by design: use convenience methods (get/post/...) instead.
+   *
+   * @param {string} method HTTP method
+   * @param {string} url Request URL
+   * @param {{ headers?: Record<string,string>, body?: string|Buffer|any }} [config]
+   * @returns {Promise<HttpResponse>}
+   */
+  async #requestFactory(method, url, config = {}) {
+    const u = new URL(url);
+    const req = request({
+      method,
+      protocol: u.protocol,
+      hostname: u.hostname,
+      port: u.port,
+      path: u.pathname + u.search,
+      headers: config.headers || {},
+    });
+
+    // write body if provided
+    if (config.body) {
+      if (typeof config.body === "string" || Buffer.isBuffer(config.body)) {
+        req.write(config.body);
+      } else {
+        req.write(JSON.stringify(config.body));
+      }
+    }
+
+    req.end();
+
+    const incoming = await new Promise((resolve, reject) => {
+      req.on("error", reject);
+      req.on("response", resolve);
+    });
+
+    return new HttpResponse(incoming);
+  }
+
+  // Convenience methods for common HTTP verbs:
+
+  /**
+   * GET request
+   * @param {string} url
+   * @param {{ headers?: Record<string,string>, body?: string|Buffer|any }} [config]
+   */
+  get(url, config) {
+    return this.#requestFactory("GET", url, config);
+  }
+
+  /**
+   * POST request
+   */
+  post(url, config) {
+    return this.#requestFactory("POST", url, config);
+  }
+
+  /**
+   * PUT request
+   */
+  put(url, config) {
+    return this.#requestFactory("PUT", url, config);
+  }
+
+  /**
+   * PATCH request
+   */
+  patch(url, config) {
+    return this.#requestFactory("PATCH", url, config);
+  }
+
+  /**
+   * DELETE request
+   */
+  delete(url, config) {
+    return this.#requestFactory("DELETE", url, config);
+  }
+
+  /**
+   * HEAD request
+   */
+  head(url, config) {
+    return this.#requestFactory("HEAD", url, config);
+  }
+
+  /**
+   * OPTIONS request
+   */
+  options(url, config) {
+    return this.#requestFactory("OPTIONS", url, config);
+  }
+
+  /**
+   * Custom method
+   * @param {string} method
+   */
+  custom(method, url, config) {
+    return this.#requestFactory(method, url, config);
+  }
+}
+
+module.exports = { HttpClient, HttpResponse };

feat(git_ssh): make ssh clone/pull/push work again
+ 368
- 89
@@ -1,4 +1,4 @@
-FROM node:alpine as builder
+FROM node:alpine AS builder
 
 ENV NODE_ENV=development
 ENV NODE_OPTIONS=--openssl-legacy-provider

...
@@ -52,7 +52,7 @@ RUN yarn bundle:islands    # Bundle Islands (react-monolith) to ESM/CJS/UMD
 COPY ./public              /usr/src/app/public
 COPY ./app.manifest.json   /usr/src/app/app.manifest.json
 
-FROM node:slim as base
+FROM node:slim AS base
 
 ENV NODE_ENV=production
 ENV NODE_OPTIONS=--openssl-legacy-provider

...
@@ -153,9 +153,14 @@ RUN chmod +x git-shell-commands/no-interactive-login
 # Add ssh command to force client command
 COPY ./data/ssh_command /usr/bin/
 COPY ./data/ssh_command_node /usr/bin/
+COPY ./data/http_client.js /usr/bin/
 RUN chmod +x /usr/bin/ssh_command
 RUN chmod +x /usr/bin/ssh_command_node
 
+RUN touch /opt/ssh_commands.log
+RUN chown git:git /opt/ssh_commands.log
+RUN chmod 644 /opt/ssh_commands.log
+
 # Setup ssh folder and keys
 RUN mkdir -p .ssh
 RUN chmod 700 .ssh

app/controllers/ssh-auth.ts
@@ -7,7 +7,7 @@ import { makeGitServerService } from "../services/gitServer";
 
 const onSSHAuth: ReqHandler<AppRouteParams, AppRoute.SSH_AUTH> = async (
   request,
-  reply
+  reply,
 ) => {
   const gitService = makeGitServerService({
     request,

...
@@ -19,10 +19,14 @@ const onSSHAuth: ReqHandler<AppRouteParams, AppRoute.SSH_AUTH> = async (
 
   const { command, repoSlug, username, publicKey } = request.body;
 
+  console.log("SSH auth request received with body:", request.body);
+
   const result = await gitService.repositoryResolver(
-    repoSlug.replace(/\.git$/, "")
+    repoSlug.replace(/\.git$/, ""),
   );
 
+  console.log("result from repositoryResolver:", result);
+
   let { authMode, gitRepositoryDir } = result;
   gitRepositoryDir = gitRepositoryDir.toString().endsWith(".git")
     ? gitRepositoryDir

...
@@ -33,6 +37,12 @@ const onSSHAuth: ReqHandler<AppRouteParams, AppRoute.SSH_AUTH> = async (
     (authMode === GitServer.AuthMode.PUSH_ONLY &&
       command !== "git-receive-pack") // push
   ) {
+    console.log("Successful auth:", {
+      authMode,
+      command,
+      gitRepositoryDir,
+    });
+
     reply.status(200).send({
       success: true,
       authMode,

...
@@ -47,9 +57,11 @@ const onSSHAuth: ReqHandler<AppRouteParams, AppRoute.SSH_AUTH> = async (
     {
       username,
       password: publicKey,
-    }
+    },
   );
 
+  console.log("is auth valid?", isAuthorizationValid);
+
   if (isAuthorizationValid) {
     const [orgSlug, repoName] = repoSlug.replace(/\.git$/, "").split("/");
     request.prisma.repository

app/islands/RepositoryTreeView.tsx
@@ -107,13 +107,14 @@ const RepositoryTreeView: ReactIsland<
               title={"Branch"}
               onChange={(e) => {
                 console.log("branch changed to: ", e.currentTarget.value);
-                window.__router.push(
-                  buildRouteLink(AppRoute.REPOSITORY_BROWSER_WITH_PATH, {
+                window.location.href = buildRouteLink(
+                  AppRoute.REPOSITORY_BROWSER_WITH_PATH,
+                  {
                     orgSlug: orgSlug,
                     repoSlug: repoSlug,
                     currentRef: e.currentTarget.value,
                     "*": currentPath === "/" ? "" : currentPath,
-                  }),
+                  },
                 );
               }}
               style={{

app/services/gitServer/authorizationResolver.ts
@@ -18,6 +18,12 @@ const makeAuthorizationResolver: ServiceMethodFactory<
 
     const repoSlug = repoSlugUnsafe.replace(/\.(git|pub)$/, "");
 
+    console.log("AuthorizationResolver called with:", {
+      repoPath,
+      username,
+      isPubKeyAuth,
+    });
+
     const user = await request.prisma.user.findUnique({
       where: {
         username,

app/views/repository/RepositoryBrowserView.tsx
@@ -105,13 +105,14 @@ const RepositoryBrowserView: ReactView<RepositoryBrowserViewProps> = ({
                 title={"Branch"}
                 onChange={(e) => {
                   console.log("branch changed to: ", e.currentTarget.value);
-                  window.__router.push(
-                    buildRouteLink(AppRoute.REPOSITORY_BROWSER_WITH_PATH, {
+                  window.location.href = buildRouteLink(
+                    AppRoute.REPOSITORY_BROWSER_WITH_PATH,
+                    {
                       orgSlug: parentOrg.slug,
                       repoSlug: repo.slug,
                       currentRef: e.currentTarget.value,
                       "*": path,
-                    }),
+                    },
                   );
                 }}
                 style={{

new file
data/http_client.d.ts
@@ -0,0 +1,38 @@
+declare module "./http_client" {
+  import { IncomingMessage } from "http";
+
+  export class HttpResponse {
+    readonly statusCode: number;
+    readonly statusText: string;
+    readonly ok: boolean;
+    readonly headers: IncomingMessage["headers"];
+
+    constructor(incoming: IncomingMessage);
+
+    text(): Promise<string>;
+    isJson(): Promise<boolean>;
+    json(): Promise<any>;
+  }
+
+  export interface RequestConfig {
+    headers?: Record<string, string>;
+    body?: string | Buffer | any;
+  }
+
+  export class HttpClient {
+    constructor();
+
+    get(url: string, config?: RequestConfig): Promise<HttpResponse>;
+    post(url: string, config?: RequestConfig): Promise<HttpResponse>;
+    put(url: string, config?: RequestConfig): Promise<HttpResponse>;
+    patch(url: string, config?: RequestConfig): Promise<HttpResponse>;
+    delete(url: string, config?: RequestConfig): Promise<HttpResponse>;
+    head(url: string, config?: RequestConfig): Promise<HttpResponse>;
+    options(url: string, config?: RequestConfig): Promise<HttpResponse>;
+    custom(
+      method: string,
+      url: string,
+      config?: RequestConfig,
+    ): Promise<HttpResponse>;
+  }
+}

new file
data/http_client.js
@@ -0,0 +1,204 @@
+/**
+ * Minimal HTTP client using only Node.js built-ins.
+ *
+ * Exports:
+ *   - HttpClient: class with methods get/post/put/patch/delete/head/options/custom
+ *   - HttpResponse: response wrapper (left unchanged per request)
+ *
+ * HttpResponse API:
+ *   - statusCode: number
+ *   - statusText: string
+ *   - ok: boolean (2xx)
+ *   - headers: object
+ *   - text(): Promise<string>
+ *   - isJson(): Promise<boolean>
+ *   - json(): Promise<any>
+ *
+ * HttpClient usage (example):
+ *   const { HttpClient } = require('./client');
+ *   const client = new HttpClient();
+ *   const res = await client.get('http://localhost:3000/auth', {
+ *     headers: { 'content-type':'application/json' },
+ *     // body: ..., // .post/.put/.patch only
+ *   });
+ *   if (!res.ok) {
+ *     const txt = await res.text();
+ *     return txt;
+ *   }
+ *   if (!await res.isJson()) {
+ *     const txt = await res.text();
+ *     return txt;
+ *   }
+ *   const obj = await res.json();
+ *   // ... whatever with obj ...
+ */
+
+const { request } = require("http");
+const { URL } = require("url");
+
+class HttpResponse {
+  /**
+   * Wrap an IncomingMessage.
+   * @param {import('http').IncomingMessage} incoming
+   */
+  constructor(incoming) {
+    this._incoming = incoming;
+    this._buf = null;
+    this.statusCode = incoming.statusCode || 0;
+    this.statusText = incoming.statusMessage || "";
+    this.ok = this.statusCode >= 200 && this.statusCode < 300;
+    this.headers = incoming.headers;
+    this.header = incoming.header;
+    this._json = null;
+  }
+
+  /**
+   * Buffer the full response body (private).
+   * @returns {Promise<Buffer>}
+   */
+  async #readAll() {
+    return new Promise((resolve, reject) => {
+      const bufs = [];
+      this._incoming.on("data", (b) => bufs.push(b));
+      this._incoming.on("end", () => resolve(Buffer.concat(bufs)));
+      this._incoming.on("error", reject);
+    });
+  }
+
+  /**
+   * Return the response body as a string. Buffers on first call and reuses the value.
+   * @returns {Promise<string>}
+   */
+  async text() {
+    if (!this._buf) this._buf = await this.#readAll();
+    return this._buf.toString();
+  }
+
+  /**
+   * Feature-detect whether the body is valid JSON.
+   * Uses text() as the single source-of-truth for the buffered body.
+   * @returns {Promise<boolean>}
+   */
+  async isJson() {
+    try {
+      this._json = JSON.parse(await this.text());
+      return true;
+    } catch {
+      return false;
+    }
+  }
+
+  /**
+   * Parse and return JSON body. Throws if body is not JSON.
+   * @returns {Promise<any>}
+   */
+  async json() {
+    if ((await this.isJson()) === false) throw new Error("not json");
+    if (this._json == null) this._json = JSON.parse(await this.text());
+    return this._json;
+  }
+}
+
+class HttpClient {
+  /**
+   * Internal request factory that performs a request and returns HttpResponse.
+   * Private by design: use convenience methods (get/post/...) instead.
+   *
+   * @param {string} method HTTP method
+   * @param {string} url Request URL
+   * @param {{ headers?: Record<string,string>, body?: string|Buffer|any }} [config]
+   * @returns {Promise<HttpResponse>}
+   */
+  async #requestFactory(method, url, config = {}) {
+    const u = new URL(url);
+    const req = request({
+      method,
+      protocol: u.protocol,
+      hostname: u.hostname,
+      port: u.port,
+      path: u.pathname + u.search,
+      headers: config.headers || {},
+    });
+
+    // write body if provided
+    if (config.body) {
+      if (typeof config.body === "string" || Buffer.isBuffer(config.body)) {
+        req.write(config.body);
+      } else {
+        req.write(JSON.stringify(config.body));
+      }
+    }
+
+    req.end();
+
+    const incoming = await new Promise((resolve, reject) => {
+      req.on("error", reject);
+      req.on("response", resolve);
+    });
+
+    return new HttpResponse(incoming);
+  }
+
+  // Convenience methods for common HTTP verbs:
+
+  /**
+   * GET request
+   * @param {string} url
+   * @param {{ headers?: Record<string,string>, body?: string|Buffer|any }} [config]
+   */
+  get(url, config) {
+    return this.#requestFactory("GET", url, config);
+  }
+
+  /**
+   * POST request
+   */
+  post(url, config) {
+    return this.#requestFactory("POST", url, config);
+  }
+
+  /**
+   * PUT request
+   */
+  put(url, config) {
+    return this.#requestFactory("PUT", url, config);
+  }
+
+  /**
+   * PATCH request
+   */
+  patch(url, config) {
+    return this.#requestFactory("PATCH", url, config);
+  }
+
+  /**
+   * DELETE request
+   */
+  delete(url, config) {
+    return this.#requestFactory("DELETE", url, config);
+  }
+
+  /**
+   * HEAD request
+   */
+  head(url, config) {
+    return this.#requestFactory("HEAD", url, config);
+  }
+
+  /**
+   * OPTIONS request
+   */
+  options(url, config) {
+    return this.#requestFactory("OPTIONS", url, config);
+  }
+
+  /**
+   * Custom method
+   * @param {string} method
+   */
+  custom(method, url, config) {
+    return this.#requestFactory(method, url, config);
+  }
+}
+
+module.exports = { HttpClient, HttpResponse };

@@ -17,19 +17,21 @@ USERNAME=$1
 RES_JSON=$(/usr/bin/ssh_command_node "${USERNAME}")
 EXIT=$?
 
-echo "result => (${EXIT})\n-----------\n\n" >> /home/git/ssh_commands.log
+echo "===> ${RES_JSON}\n"  >> /opt/ssh_commands.log
+
+echo "result => (${EXIT})\n-----------\n" >> /opt/ssh_commands.log
 
 COMMAND=$(echo "$RES_JSON" | jq -r '.command')
 AUTH_MODE=$(echo "$RES_JSON" | jq -r '.authMode')
 GIT_REPO_DIR=$(echo "$RES_JSON" | jq -r '.gitRepositoryDir')
 
-echo "AUTH_MODE: ${AUTH_MODE}" >> /home/git/ssh_commands.log
-echo "GIT_REPO_DIR: ${GIT_REPO_DIR}" >> /home/git/ssh_commands.log
-echo "ssh_command_node stdout: ${RES_JSON}" >> /home/git/ssh_commands.log
-echo "ssh_command_node exit code: ${EXIT}" >> /home/git/ssh_commands.log
+echo "AUTH_MODE: ${AUTH_MODE}" >> /opt/ssh_commands.log
+echo "GIT_REPO_DIR: ${GIT_REPO_DIR}" >> /opt/ssh_commands.log
+echo "ssh_command_node stdout: ${RES_JSON}" >> /opt/ssh_commands.log
+echo "ssh_command_node exit code: ${EXIT}" >> /opt/ssh_commands.log
 
 if [ "$EXIT" = "0" ]; then
-  $COMMAND $GIT_REPO_DIR;
+  LANG=C $COMMAND $GIT_REPO_DIR;
   exit $?
 else
   echo "Forbidden access.\n"

data/ssh_command_node
@@ -1,8 +1,27 @@
 #!/usr/bin/node
 
-const http = require("http");
+const { HttpClient } = require("./http_client");
+// const { HttpClient } = require("/home/debian/http_client.js");
 const fs = require("fs");
 
+const LOGS_FILE = "/opt/ssh_commands.log";
+
+function log(message, ...args) {
+  try {
+    fs.appendFileSync(
+      LOGS_FILE,
+      JSON.stringify({
+        timestamp: new Date().toISOString(),
+        message,
+        args,
+      }) + "\n",
+      { encoding: "utf8" },
+    );
+  } catch (err) {
+    // console.log(message, ...args);
+  }
+}
+
 async function main(args, sshOriginalCommand) {
   const [_, __, username] = args;
 

...
@@ -30,19 +49,22 @@ async function main(args, sshOriginalCommand) {
     )
     .filter((x) => x != null && x.type === "key");
 
-  console.log("authkeys:", authKeys);
-  console.log("username", username);
+  // console.log("authkeys:", authKeys);
+  // console.log("username", username);
 
-  let pk = authKeys.find(
+  let userPk = authKeys.find(
     (key) =>
       key.text.includes(`command="ssh_command ${username}"`) ||
       key.text.includes(`command="/usr/bin/ssh_command ${username}"`),
   );
 
-  if (pk) {
-    pk = pk.text;
+  if (userPk == null) {
+    log("No key matched ssh connection in authorized_keys file.", { username });
+    return process.exit(128);
   }
 
+  const pk = userPk.text;
+
   const sshRsaIndex = pk.indexOf("ssh-rsa");
   const publicKey = pk.substring(sshRsaIndex);
 

...
@@ -50,12 +72,6 @@ async function main(args, sshOriginalCommand) {
     .split(" ")
     .map((part) => part.replace(/\'/g, "").trim());
 
-  fs.appendFileSync(
-    "/home/git/ssh_commands.log",
-    `username: ${username}\npublicKey: ${publicKey}\ncommand: ${command}\nrepoSlug: ${repoSlug}\n-----------\n`,
-    { encoding: "utf8" },
-  );
-
   const data = JSON.stringify({
     command,
     repoSlug,

...
@@ -63,75 +79,66 @@ async function main(args, sshOriginalCommand) {
     publicKey,
   });
 
-  const options = {
-    hostname: "localhost",
-    port: 1337,
-    path: "/_ssh/auth",
-    method: "POST",
+  log("Will authenticate through /_ssh/auth", {
+    command,
+    repoSlug,
+    username,
+    publicKey,
+  });
+
+  const client = new HttpClient();
+
+  // console.log("HttpClient.instance:", client);
+
+  const res = await client.post("http://localhost:1337/_ssh/auth", {
     headers: {
       "Content-Type": "application/json",
       "Content-Length": Buffer.byteLength(data),
     },
-  };
-
-  let json = {};
-
-  const req = http.request(options, (res) => {
-    let chunks = [];
-
-    res.on("data", (chunk) => {
-      chunks.push(chunk);
-    });
-
-    res.on("end", () => {
-      const responseBody = Buffer.concat(chunks).toString();
-
-      if (res.statusCode >= 400) {
-        // Log error details
-        fs.appendFileSync(
-          "/home/git/ssh_commands.log",
-          `${res.statusCode}: ${res.statusMessage} - ${responseBody}\n-----------\n`,
-          { encoding: "utf8" },
-        );
-        process.exit(128);
-      } else {
-        // Parse JSON response
-        try {
-          json = JSON.parse(responseBody);
-          // Do something with json if needed
-        } catch (e) {
-          // handle JSON parse error if necessary
-        }
-      }
-    });
+    body: data,
   });
 
-  req.on("error", (e) => {
-    // handle request error
-    fs.appendFileSync(
-      "/home/git/ssh_commands.log",
-      `Request error: ${e.message}\n-----------\n`,
-      { encoding: "utf8" },
+  // console.log("res:", res);
+
+  if (res.ok === false) {
+    log(
+      "/_ssh/auth response is an error:",
+      res.statusCode,
+      res.statusText,
+      await res.text(),
     );
-    process.exit(128);
-  });
+    return process.exit(128);
+  }
 
-  // Write data to request body
-  req.write(data);
-  req.end();
+  if ((await res.isJson()) === false) {
+    log(
+      "/_ssh/auth response is not json:",
+      res.statusCode,
+      res.statusText,
+      await res.text(),
+    );
+    return process.exit(128);
+  }
 
-  // fs.appendFileSync(
-  //   "/home/git/ssh_commands.log",
-  //   `${res.status}: ${res.statusText} - ${JSON.stringify(json)}\n-----------\n`,
-  //   { encoding: "utf8" },
-  // );
+  const json = await res.json();
+
+  log("/_ssh/auth response json:", res.statusCode, res.statusText, json);
 
   if (json.success === false) {
-    process.exit(128);
+    log(
+      "/_ssh/auth response is not successful:",
+      res.statusCode,
+      res.statusText,
+      json,
+    );
+    return process.exit(128);
   }
 
-  // success!
+  json.gitRepositoryDir = `/home/debian/data/gitfoss_repos/${repoSlug.replace(/\.git$/, "")}`;
   console.log(JSON.stringify(json));
+
+  // success!
+  log("/_ssh/auth response success!", json);
   process.exit(0);
 }
 

docker-compose.caddy.yml
@@ -44,6 +44,9 @@ services:
       - reverse-proxy-public
     volumes:
       - ./data/gitfoss_repos:/var/lib/gitfoss/repos
+      - ./data/ssh_commands.log:/opt/ssh_commands.log
+      - /usr/bin/ssh_command:/usr/bin/ssh_command
+      - /usr/bin/ssh_command_node:/usr/bin/ssh_command_node
       #- /home/git/repos:/var/lib/gitfoss/repos
 
 networks:

scripts/docker-build-scp-deploy.sh
@@ -100,6 +100,6 @@ build_and_deploy \
   ${DOCKER_IMAGE_NAME:-gitfoss_web} \
   ${DOCKER_IMAGE_TAG:-latest} \
   ${REMOTE_SSH_USER:-debian} \
-  ${REMOTE_SSH_HOST:-92.243.16.118} \
-  ${REMOTE_SSH_PORT:-22} \
+  ${REMOTE_SSH_HOST:-gitfoss.dev} \
+  ${REMOTE_SSH_PORT:-1338} \
   ${REMOTE_DESTINATION_PATH:-/home/debian/}

@@ -1,14 +1,14 @@
 todo:
 
-- [ ] make ssh server work every times
+- [ ] make ssh server work every times !!!!!
 - [x] make the islands runtime load dependencies properly
 - [ ] finish merge pull request feature
 - [x] add ssh key feature
 - [ ] add update/delete ssh key feature
 - [ ] add a function to change default branch -> update repo/HEAD ref, update config->init.defaultBranch
 - [ ] add a function to enable/disable fast forward push
-- [ ] (?) allow to write comments on a PR 
+- [ ] (?) allow to write comments on a PR
 - [ ] update repo/description file when repo name or description is set (create, update)
-- [ ] add a select button to change branch/tag (repository browser)
+- [x] add a select button to change branch/tag (repository browser)
 - [ ] make it possible to edit user profile (avatar, display name, email, password, etc)
 - [ ] make it possible to change app theme (colors) via config file

@@ -17,19 +17,21 @@ USERNAME=$1
 RES_JSON=$(/usr/bin/ssh_command_node "${USERNAME}")
 EXIT=$?
 
-echo "result => (${EXIT})\n-----------\n\n" >> /home/git/ssh_commands.log
+echo "===> ${RES_JSON}\n"  >> /opt/ssh_commands.log
+
+echo "result => (${EXIT})\n-----------\n" >> /opt/ssh_commands.log
 
 COMMAND=$(echo "$RES_JSON" | jq -r '.command')
 AUTH_MODE=$(echo "$RES_JSON" | jq -r '.authMode')
 GIT_REPO_DIR=$(echo "$RES_JSON" | jq -r '.gitRepositoryDir')
 
-echo "AUTH_MODE: ${AUTH_MODE}" >> /home/git/ssh_commands.log
-echo "GIT_REPO_DIR: ${GIT_REPO_DIR}" >> /home/git/ssh_commands.log
-echo "ssh_command_node stdout: ${RES_JSON}" >> /home/git/ssh_commands.log
-echo "ssh_command_node exit code: ${EXIT}" >> /home/git/ssh_commands.log
+echo "AUTH_MODE: ${AUTH_MODE}" >> /opt/ssh_commands.log
+echo "GIT_REPO_DIR: ${GIT_REPO_DIR}" >> /opt/ssh_commands.log
+echo "ssh_command_node stdout: ${RES_JSON}" >> /opt/ssh_commands.log
+echo "ssh_command_node exit code: ${EXIT}" >> /opt/ssh_commands.log
 
 if [ "$EXIT" = "0" ]; then
-  $COMMAND $GIT_REPO_DIR;
+  LANG=C $COMMAND $GIT_REPO_DIR;
   exit $?
 else
   echo "Forbidden access.\n"

data/ssh_command_node
@@ -1,8 +1,27 @@
 #!/usr/bin/node
 
-const http = require("http");
+const { HttpClient } = require("./http_client");
+// const { HttpClient } = require("/home/debian/http_client.js");
 const fs = require("fs");
 
+const LOGS_FILE = "/opt/ssh_commands.log";
+
+function log(message, ...args) {
+  try {
+    fs.appendFileSync(
+      LOGS_FILE,
+      JSON.stringify({
+        timestamp: new Date().toISOString(),
+        message,
+        args,
+      }) + "\n",
+      { encoding: "utf8" },
+    );
+  } catch (err) {
+    // console.log(message, ...args);
+  }
+}
+
 async function main(args, sshOriginalCommand) {
   const [_, __, username] = args;
 

...
@@ -30,19 +49,22 @@ async function main(args, sshOriginalCommand) {
     )
     .filter((x) => x != null && x.type === "key");
 
-  console.log("authkeys:", authKeys);
-  console.log("username", username);
+  // console.log("authkeys:", authKeys);
+  // console.log("username", username);
 
-  let pk = authKeys.find(
+  let userPk = authKeys.find(
     (key) =>
       key.text.includes(`command="ssh_command ${username}"`) ||
       key.text.includes(`command="/usr/bin/ssh_command ${username}"`),
   );
 
-  if (pk) {
-    pk = pk.text;
+  if (userPk == null) {
+    log("No key matched ssh connection in authorized_keys file.", { username });
+    return process.exit(128);
   }
 
+  const pk = userPk.text;
+
   const sshRsaIndex = pk.indexOf("ssh-rsa");
   const publicKey = pk.substring(sshRsaIndex);
 

...
@@ -50,12 +72,6 @@ async function main(args, sshOriginalCommand) {
     .split(" ")
     .map((part) => part.replace(/\'/g, "").trim());
 
-  fs.appendFileSync(
-    "/home/git/ssh_commands.log",
-    `username: ${username}\npublicKey: ${publicKey}\ncommand: ${command}\nrepoSlug: ${repoSlug}\n-----------\n`,
-    { encoding: "utf8" },
-  );
-
   const data = JSON.stringify({
     command,
     repoSlug,

...
@@ -63,75 +79,66 @@ async function main(args, sshOriginalCommand) {
     publicKey,
   });
 
-  const options = {
-    hostname: "localhost",
-    port: 1337,
-    path: "/_ssh/auth",
-    method: "POST",
+  log("Will authenticate through /_ssh/auth", {
+    command,
+    repoSlug,
+    username,
+    publicKey,
+  });
+
+  const client = new HttpClient();
+
+  // console.log("HttpClient.instance:", client);
+
+  const res = await client.post("http://localhost:1337/_ssh/auth", {
     headers: {
       "Content-Type": "application/json",
       "Content-Length": Buffer.byteLength(data),
     },
-  };
-
-  let json = {};
-
-  const req = http.request(options, (res) => {
-    let chunks = [];
-
-    res.on("data", (chunk) => {
-      chunks.push(chunk);
-    });
-
-    res.on("end", () => {
-      const responseBody = Buffer.concat(chunks).toString();
-
-      if (res.statusCode >= 400) {
-        // Log error details
-        fs.appendFileSync(
-          "/home/git/ssh_commands.log",
-          `${res.statusCode}: ${res.statusMessage} - ${responseBody}\n-----------\n`,
-          { encoding: "utf8" },
-        );
-        process.exit(128);
-      } else {
-        // Parse JSON response
-        try {
-          json = JSON.parse(responseBody);
-          // Do something with json if needed
-        } catch (e) {
-          // handle JSON parse error if necessary
-        }
-      }
-    });
+    body: data,
   });
 
-  req.on("error", (e) => {
-    // handle request error
-    fs.appendFileSync(
-      "/home/git/ssh_commands.log",
-      `Request error: ${e.message}\n-----------\n`,
-      { encoding: "utf8" },
+  // console.log("res:", res);
+
+  if (res.ok === false) {
+    log(
+      "/_ssh/auth response is an error:",
+      res.statusCode,
+      res.statusText,
+      await res.text(),
     );
-    process.exit(128);
-  });
+    return process.exit(128);
+  }
 
-  // Write data to request body
-  req.write(data);
-  req.end();
+  if ((await res.isJson()) === false) {
+    log(
+      "/_ssh/auth response is not json:",
+      res.statusCode,
+      res.statusText,
+      await res.text(),
+    );
+    return process.exit(128);
+  }
 
-  // fs.appendFileSync(
-  //   "/home/git/ssh_commands.log",
-  //   `${res.status}: ${res.statusText} - ${JSON.stringify(json)}\n-----------\n`,
-  //   { encoding: "utf8" },
-  // );
+  const json = await res.json();
+
+  log("/_ssh/auth response json:", res.statusCode, res.statusText, json);
 
   if (json.success === false) {
-    process.exit(128);
+    log(
+      "/_ssh/auth response is not successful:",
+      res.statusCode,
+      res.statusText,
+      json,
+    );
+    return process.exit(128);
   }
 
-  // success!
+  json.gitRepositoryDir = `/home/debian/data/gitfoss_repos/${repoSlug.replace(/\.git$/, "")}`;
   console.log(JSON.stringify(json));
+
+  // success!
+  log("/_ssh/auth response success!", json);
   process.exit(0);
 }
 

docker-compose.caddy.yml
@@ -44,6 +44,9 @@ services:
       - reverse-proxy-public
     volumes:
       - ./data/gitfoss_repos:/var/lib/gitfoss/repos
+      - ./data/ssh_commands.log:/opt/ssh_commands.log
+      - /usr/bin/ssh_command:/usr/bin/ssh_command
+      - /usr/bin/ssh_command_node:/usr/bin/ssh_command_node
       #- /home/git/repos:/var/lib/gitfoss/repos
 
 networks:

scripts/docker-build-scp-deploy.sh
@@ -100,6 +100,6 @@ build_and_deploy \
   ${DOCKER_IMAGE_NAME:-gitfoss_web} \
   ${DOCKER_IMAGE_TAG:-latest} \
   ${REMOTE_SSH_USER:-debian} \
-  ${REMOTE_SSH_HOST:-92.243.16.118} \
-  ${REMOTE_SSH_PORT:-22} \
+  ${REMOTE_SSH_HOST:-gitfoss.dev} \
+  ${REMOTE_SSH_PORT:-1338} \
   ${REMOTE_DESTINATION_PATH:-/home/debian/}

@@ -1,14 +1,14 @@
 todo:
 
-- [ ] make ssh server work every times
+- [ ] make ssh server work every times !!!!!
 - [x] make the islands runtime load dependencies properly
 - [ ] finish merge pull request feature
 - [x] add ssh key feature
 - [ ] add update/delete ssh key feature
 - [ ] add a function to change default branch -> update repo/HEAD ref, update config->init.defaultBranch
 - [ ] add a function to enable/disable fast forward push
-- [ ] (?) allow to write comments on a PR 
+- [ ] (?) allow to write comments on a PR
 - [ ] update repo/description file when repo name or description is set (create, update)
-- [ ] add a select button to change branch/tag (repository browser)
+- [x] add a select button to change branch/tag (repository browser)
 - [ ] make it possible to edit user profile (avatar, display name, email, password, etc)
 - [ ] make it possible to change app theme (colors) via config file

data/ssh_command
@@ -17,19 +17,21 @@ USERNAME=$1
 RES_JSON=$(/usr/bin/ssh_command_node "${USERNAME}")
 EXIT=$?
 
-echo "result => (${EXIT})\n-----------\n\n" >> /home/git/ssh_commands.log
+echo "===> ${RES_JSON}\n"  >> /opt/ssh_commands.log
+
+echo "result => (${EXIT})\n-----------\n" >> /opt/ssh_commands.log
 
 COMMAND=$(echo "$RES_JSON" | jq -r '.command')
 AUTH_MODE=$(echo "$RES_JSON" | jq -r '.authMode')
 GIT_REPO_DIR=$(echo "$RES_JSON" | jq -r '.gitRepositoryDir')
 
-echo "AUTH_MODE: ${AUTH_MODE}" >> /home/git/ssh_commands.log
-echo "GIT_REPO_DIR: ${GIT_REPO_DIR}" >> /home/git/ssh_commands.log
-echo "ssh_command_node stdout: ${RES_JSON}" >> /home/git/ssh_commands.log
-echo "ssh_command_node exit code: ${EXIT}" >> /home/git/ssh_commands.log
+echo "AUTH_MODE: ${AUTH_MODE}" >> /opt/ssh_commands.log
+echo "GIT_REPO_DIR: ${GIT_REPO_DIR}" >> /opt/ssh_commands.log
+echo "ssh_command_node stdout: ${RES_JSON}" >> /opt/ssh_commands.log
+echo "ssh_command_node exit code: ${EXIT}" >> /opt/ssh_commands.log
 
 if [ "$EXIT" = "0" ]; then
-  $COMMAND $GIT_REPO_DIR;
+  LANG=C $COMMAND $GIT_REPO_DIR;
   exit $?
 else
   echo "Forbidden access.\n"

data/ssh_command_node
@@ -1,8 +1,27 @@
 #!/usr/bin/node
 
-const http = require("http");
+const { HttpClient } = require("./http_client");
+// const { HttpClient } = require("/home/debian/http_client.js");
 const fs = require("fs");
 
+const LOGS_FILE = "/opt/ssh_commands.log";
+
+function log(message, ...args) {
+  try {
+    fs.appendFileSync(
+      LOGS_FILE,
+      JSON.stringify({
+        timestamp: new Date().toISOString(),
+        message,
+        args,
+      }) + "\n",
+      { encoding: "utf8" },
+    );
+  } catch (err) {
+    // console.log(message, ...args);
+  }
+}
+
 async function main(args, sshOriginalCommand) {
   const [_, __, username] = args;
 

...
@@ -30,19 +49,22 @@ async function main(args, sshOriginalCommand) {
     )
     .filter((x) => x != null && x.type === "key");
 
-  console.log("authkeys:", authKeys);
-  console.log("username", username);
+  // console.log("authkeys:", authKeys);
+  // console.log("username", username);
 
-  let pk = authKeys.find(
+  let userPk = authKeys.find(
     (key) =>
       key.text.includes(`command="ssh_command ${username}"`) ||
       key.text.includes(`command="/usr/bin/ssh_command ${username}"`),
   );
 
-  if (pk) {
-    pk = pk.text;
+  if (userPk == null) {
+    log("No key matched ssh connection in authorized_keys file.", { username });
+    return process.exit(128);
   }
 
+  const pk = userPk.text;
+
   const sshRsaIndex = pk.indexOf("ssh-rsa");
   const publicKey = pk.substring(sshRsaIndex);
 

...
@@ -50,12 +72,6 @@ async function main(args, sshOriginalCommand) {
     .split(" ")
     .map((part) => part.replace(/\'/g, "").trim());
 
-  fs.appendFileSync(
-    "/home/git/ssh_commands.log",
-    `username: ${username}\npublicKey: ${publicKey}\ncommand: ${command}\nrepoSlug: ${repoSlug}\n-----------\n`,
-    { encoding: "utf8" },
-  );
-
   const data = JSON.stringify({
     command,
     repoSlug,

...
@@ -63,75 +79,66 @@ async function main(args, sshOriginalCommand) {
     publicKey,
   });
 
-  const options = {
-    hostname: "localhost",
-    port: 1337,
-    path: "/_ssh/auth",
-    method: "POST",
+  log("Will authenticate through /_ssh/auth", {
+    command,
+    repoSlug,
+    username,
+    publicKey,
+  });
+
+  const client = new HttpClient();
+
+  // console.log("HttpClient.instance:", client);
+
+  const res = await client.post("http://localhost:1337/_ssh/auth", {
     headers: {
       "Content-Type": "application/json",
       "Content-Length": Buffer.byteLength(data),
     },
-  };
-
-  let json = {};
-
-  const req = http.request(options, (res) => {
-    let chunks = [];
-
-    res.on("data", (chunk) => {
-      chunks.push(chunk);
-    });
-
-    res.on("end", () => {
-      const responseBody = Buffer.concat(chunks).toString();
-
-      if (res.statusCode >= 400) {
-        // Log error details
-        fs.appendFileSync(
-          "/home/git/ssh_commands.log",
-          `${res.statusCode}: ${res.statusMessage} - ${responseBody}\n-----------\n`,
-          { encoding: "utf8" },
-        );
-        process.exit(128);
-      } else {
-        // Parse JSON response
-        try {
-          json = JSON.parse(responseBody);
-          // Do something with json if needed
-        } catch (e) {
-          // handle JSON parse error if necessary
-        }
-      }
-    });
+    body: data,
   });
 
-  req.on("error", (e) => {
-    // handle request error
-    fs.appendFileSync(
-      "/home/git/ssh_commands.log",
-      `Request error: ${e.message}\n-----------\n`,
-      { encoding: "utf8" },
+  // console.log("res:", res);
+
+  if (res.ok === false) {
+    log(
+      "/_ssh/auth response is an error:",
+      res.statusCode,
+      res.statusText,
+      await res.text(),
     );
-    process.exit(128);
-  });
+    return process.exit(128);
+  }
 
-  // Write data to request body
-  req.write(data);
-  req.end();
+  if ((await res.isJson()) === false) {
+    log(
+      "/_ssh/auth response is not json:",
+      res.statusCode,
+      res.statusText,
+      await res.text(),
+    );
+    return process.exit(128);
+  }
 
-  // fs.appendFileSync(
-  //   "/home/git/ssh_commands.log",
-  //   `${res.status}: ${res.statusText} - ${JSON.stringify(json)}\n-----------\n`,
-  //   { encoding: "utf8" },
-  // );
+  const json = await res.json();
+
+  log("/_ssh/auth response json:", res.statusCode, res.statusText, json);
 
   if (json.success === false) {
-    process.exit(128);
+    log(
+      "/_ssh/auth response is not successful:",
+      res.statusCode,
+      res.statusText,
+      json,
+    );
+    return process.exit(128);
   }
 
-  // success!
+  json.gitRepositoryDir = `/home/debian/data/gitfoss_repos/${repoSlug.replace(/\.git$/, "")}`;
   console.log(JSON.stringify(json));
+
+  // success!
+  log("/_ssh/auth response success!", json);
   process.exit(0);
 }
 

docker-compose.caddy.yml
@@ -44,6 +44,9 @@ services:
       - reverse-proxy-public
     volumes:
       - ./data/gitfoss_repos:/var/lib/gitfoss/repos
+      - ./data/ssh_commands.log:/opt/ssh_commands.log
+      - /usr/bin/ssh_command:/usr/bin/ssh_command
+      - /usr/bin/ssh_command_node:/usr/bin/ssh_command_node
       #- /home/git/repos:/var/lib/gitfoss/repos
 
 networks:

scripts/docker-build-scp-deploy.sh
@@ -100,6 +100,6 @@ build_and_deploy \
   ${DOCKER_IMAGE_NAME:-gitfoss_web} \
   ${DOCKER_IMAGE_TAG:-latest} \
   ${REMOTE_SSH_USER:-debian} \
-  ${REMOTE_SSH_HOST:-92.243.16.118} \
-  ${REMOTE_SSH_PORT:-22} \
+  ${REMOTE_SSH_HOST:-gitfoss.dev} \
+  ${REMOTE_SSH_PORT:-1338} \
   ${REMOTE_DESTINATION_PATH:-/home/debian/}

todo.md
@@ -1,14 +1,14 @@
 todo:
 
-- [ ] make ssh server work every times
+- [ ] make ssh server work every times !!!!!
 - [x] make the islands runtime load dependencies properly
 - [ ] finish merge pull request feature
 - [x] add ssh key feature
 - [ ] add update/delete ssh key feature
 - [ ] add a function to change default branch -> update repo/HEAD ref, update config->init.defaultBranch
 - [ ] add a function to enable/disable fast forward push
-- [ ] (?) allow to write comments on a PR 
+- [ ] (?) allow to write comments on a PR
 - [ ] update repo/description file when repo name or description is set (create, update)
-- [ ] add a select button to change branch/tag (repository browser)
+- [x] add a select button to change branch/tag (repository browser)
 - [ ] make it possible to edit user profile (avatar, display name, email, password, etc)
 - [ ] make it possible to change app theme (colors) via config file

GitFOSS • v0.2.0 (#421408f) • MIT License