Copilot commented on code in PR #3450:
URL: 
https://github.com/apache/incubator-kie-tools/pull/3450#discussion_r2820961479


##########
packages/accelerator-git-http-server/tests/integration.test.ts:
##########
@@ -0,0 +1,197 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import * as http from "http";
+import * as fs from "fs";
+import * as path from "path";
+import { startGitHttpServer } from "../src/server";
+
+async function startServerAndWait(config: {
+  port: number;
+  contentRoot: string;
+  logPrefix?: string;
+}): Promise<http.Server> {
+  const server = startGitHttpServer(config);
+  return new Promise((resolve) => {
+    server.on("listening", () => resolve(server));
+  });
+}
+
+describe("Integration Tests", () => {
+  let server: http.Server;
+  let testPort = 9881;
+  const testContentRoot = path.join(__dirname, 
"../dist-tests/test-integration-content");
+
+  beforeEach(() => {
+    if (!fs.existsSync(testContentRoot)) {
+      fs.mkdirSync(testContentRoot, { recursive: true });
+    }
+  });
+
+  afterEach(async () => {
+    if (server) {
+      await new Promise<void>((resolve, reject) => {
+        server.close((err) => {
+          if (err) reject(err);
+          else resolve();
+        });
+      });
+      server = null as any;
+    }
+
+    if (fs.existsSync(testContentRoot)) {
+      fs.rmSync(testContentRoot, { recursive: true, force: true });
+    }
+
+    testPort++;
+  });
+
+  describe("Mixed content serving", () => {
+    it("should serve both Git repos and static content simultaneously", async 
() => {
+      // Setup
+      fs.writeFileSync(path.join(testContentRoot, "index.html"), 
"<html><body>Test</body></html>");
+      fs.mkdirSync(path.join(testContentRoot, "test.git"), { recursive: true 
});
+
+      server = await startServerAndWait({
+        port: testPort,
+        contentRoot: testContentRoot,
+      });
+
+      // Test static content and Git repo endpoint concurrently
+      const [staticRes, gitRes] = await Promise.all([
+        fetch(`http://localhost:${testPort}/index.html`),
+        fetch(`http://localhost:${testPort}/test.git/info/refs`),
+      ]);
+
+      expect(staticRes.status).toBe(200);
+      expect(gitRes.status).toBeDefined();
+    });
+
+    it("should handle multiple concurrent requests", async () => {
+      const files = ["file1.txt", "file2.txt", "file3.txt", "file4.txt"];
+      files.forEach((file) => {
+        fs.writeFileSync(path.join(testContentRoot, file), `Content of 
${file}`);
+      });
+
+      server = await startServerAndWait({
+        port: testPort,
+        contentRoot: testContentRoot,
+      });
+
+      const responses = await Promise.all(files.map((file) => 
fetch(`http://localhost:${testPort}/${file}`)));
+
+      responses.forEach((res) => {
+        expect(res.status).toBe(200);
+      });
+    });
+  });
+
+  describe("Content type handling", () => {
+    it("should serve HTML files with correct content type", async () => {
+      fs.writeFileSync(path.join(testContentRoot, "test.html"), 
"<html></html>");
+
+      server = await startServerAndWait({
+        port: testPort,
+        contentRoot: testContentRoot,
+      });
+
+      const res = await fetch(`http://localhost:${testPort}/test.html`);
+      expect(res.headers.get("content-type")).toContain("text/html");
+    });
+
+    it("should serve JSON files with correct content type", async () => {
+      fs.writeFileSync(path.join(testContentRoot, "data.json"), '{"key": 
"value"}');
+
+      server = await startServerAndWait({
+        port: testPort,
+        contentRoot: testContentRoot,
+      });
+
+      const res = await fetch(`http://localhost:${testPort}/data.json`);
+      expect(res.headers.get("content-type")).toContain("application/json");
+    });
+
+    it("should serve CSS files with correct content type", async () => {
+      fs.writeFileSync(path.join(testContentRoot, "styles.css"), "body { 
margin: 0; }");
+
+      server = await startServerAndWait({
+        port: testPort,
+        contentRoot: testContentRoot,
+      });
+
+      const res = await fetch(`http://localhost:${testPort}/styles.css`);
+      expect(res.headers.get("content-type")).toContain("text/css");
+    });
+  });
+
+  describe("Directory structure", () => {
+    it("should handle nested directory structures", async () => {
+      const nestedPath = path.join(testContentRoot, "level1", "level2", 
"level3");
+      fs.mkdirSync(nestedPath, { recursive: true });
+      fs.writeFileSync(path.join(nestedPath, "deep.txt"), "Deep content");
+
+      server = await startServerAndWait({
+        port: testPort,
+        contentRoot: testContentRoot,
+      });
+
+      const res = await 
fetch(`http://localhost:${testPort}/level1/level2/level3/deep.txt`);
+      const data = await res.text();
+
+      expect(res.status).toBe(200);
+      expect(data).toBe("Deep content");
+    });
+
+    it("should list multiple Git repositories and static directories", async 
() => {
+      const dirs = ["repo1.git", "repo2.git", "static1", "static2"];
+      dirs.forEach((dir) => {
+        fs.mkdirSync(path.join(testContentRoot, dir), { recursive: true });
+      });
+
+      const consoleSpy = jest.spyOn(console, "log").mockImplementation();
+
+      server = await startServerAndWait({
+        port: testPort,
+        contentRoot: testContentRoot,
+      });
+
+      expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("Found 2 
bare Git repo(s)"));
+      expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("Found 2 
static content dir(s)"));
+      consoleSpy.mockRestore();
+    });
+  });
+
+  describe("Server configuration", () => {
+    it("should respect custom log prefix in all operations", async () => {
+      const customPrefix = "custom-test-prefix";
+      const consoleSpy = jest.spyOn(console, "log").mockImplementation();
+
+      server = await startServerAndWait({
+        port: testPort,
+        contentRoot: testContentRoot,
+        logPrefix: customPrefix,
+      });
+
+      const allLogs = consoleSpy.mock.calls.map((call) => call[0]).join("\n");
+      expect(allLogs).toContain(`[${customPrefix}]`);
+      expect(allLogs).not.toContain("[git-repo-http-dev-server]");
+      consoleSpy.mockRestore();
+    });
+  });

Review Comment:
   The test suite lacks coverage for error scenarios in the Git HTTP server 
functionality. Important error cases that should be tested include:
   - What happens when the git http-backend process fails or is not available
   - How the server handles malformed Git requests
   - Error handling when the contentRoot directory becomes unavailable after 
server start
   - Network errors during Git operations
   
   Consider adding test cases for these error scenarios to ensure the server 
handles failures gracefully.
   ```suggestion
     });
   
     describe("Error handling", () => {
       it("should return an error when accessing a missing Git repository", 
async () => {
         // Do not create the repository; simulate missing backend/repo
         server = await startServerAndWait({
           port: testPort,
           contentRoot: testContentRoot,
         });
   
         const res = await fetch(
           
`http://localhost:${testPort}/missing-repo.git/info/refs?service=git-upload-pack`,
         );
   
         // Any 4xx/5xx response indicates the server handled the failure path
         expect(res.status).toBeGreaterThanOrEqual(400);
       });
   
       it("should handle malformed Git service requests gracefully", async () 
=> {
         const repoName = "malformed.git";
         const repoPath = path.join(testContentRoot, repoName);
         fs.mkdirSync(repoPath, { recursive: true });
   
         server = await startServerAndWait({
           port: testPort,
           contentRoot: testContentRoot,
         });
   
         const res = await fetch(
           `http://localhost:${testPort}/${repoName}/git-upload-pack`,
           {
             method: "POST",
             headers: {
               "Content-Type": "application/x-git-upload-pack-request",
             },
             // Intentionally invalid payload for Git smart HTTP
             body: "not-a-valid-git-packet",
           },
         );
   
         // Expect the server to reject the malformed request
         expect(res.status).not.toBe(200);
       });
   
       it("should respond with an error if contentRoot becomes unavailable 
after start", async () => {
         const filePath = path.join(testContentRoot, "temp.txt");
         fs.writeFileSync(filePath, "temporary content");
   
         server = await startServerAndWait({
           port: testPort,
           contentRoot: testContentRoot,
         });
   
         // Remove the contentRoot while the server is running
         fs.rmSync(testContentRoot, { recursive: true, force: true });
   
         const res = await fetch(`http://localhost:${testPort}/temp.txt`);
   
         // The server should not return a successful response for missing 
content root
         expect(res.status).not.toBe(200);
       });
   
       it("should continue serving requests after a client-side network error", 
async () => {
         const staticFileName = "network-test.txt";
         const staticFilePath = path.join(testContentRoot, staticFileName);
         fs.writeFileSync(staticFilePath, "network test content");
   
         server = await startServerAndWait({
           port: testPort,
           contentRoot: testContentRoot,
         });
   
         // Simulate a client aborting a request mid-operation
         await expect(
           new Promise<void>((resolve, reject) => {
             const req = http.request(
               {
                 hostname: "localhost",
                 port: testPort,
                 path: `/${staticFileName}`,
                 method: "GET",
               },
               (res) => {
                 // Consume and ignore any data if we get a response
                 res.on("data", () => {});
                 res.on("end", () => resolve());
               },
             );
   
             req.on("error", (err) => {
               // From the client's perspective this is a network error
               reject(err);
             });
   
             req.end();
             // Abort immediately to simulate a broken connection
             req.abort();
           }),
         ).rejects.toBeInstanceOf(Error);
   
         // After the abort, the server should still serve subsequent requests
         const res = await 
fetch(`http://localhost:${testPort}/${staticFileName}`);
         const data = await res.text();
   
         expect(res.status).toBe(200);
         expect(data).toBe("network test content");
       });
     });
   ```



##########
packages/accelerator-git-http-server/src/server.ts:
##########
@@ -0,0 +1,193 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import * as http from "http";
+import * as path from "path";
+import * as fs from "fs";
+import { spawn } from "child_process";
+import serveStatic from "serve-static";
+import finalhandler from "finalhandler";
+
+export interface GitHttpServerConfig {
+  port: number;
+  contentRoot: string;
+  logPrefix?: string;
+}
+
+const gitHttpBackendVariableNames = [
+  "QUERY_STRING",
+  "REMOTE_USER",
+  "CONTENT_LENGTH",
+  "HTTP_CONTENT_ENCODING",
+  "REMOTE_USER",
+  "REMOTE_ADDR",
+  "GIT_COMMITTER_NAME",
+  "GIT_COMMITTER_EMAIL",
+  "CONTENT_TYPE",
+  "PATH_INFO",
+  "GIT_PROJECT_ROOT",
+  "PATH_TRANSLATED",
+  "SERVER_PROTOCOL",
+  "REQUEST_METHOD",
+  "GIT_HTTP_EXPORT_ALL",
+  "GIT_HTTP_MAX_REQUEST_BUFFER",
+];
+
+interface BufferState {
+  header: Buffer[];
+  body: Buffer[];
+  completedHeader: boolean;
+}
+
+/**
+ * Creates and starts a Git HTTP server
+ * @param options Server configuration options
+ * @returns HTTP server instance
+ */
+export function startGitHttpServer(options: GitHttpServerConfig): http.Server {
+  const { port, contentRoot, logPrefix = "git-repo-http-dev-server" } = 
options;
+
+  const log = (message: string) => console.log(`[${logPrefix}] ${message}`);
+
+  if (!fs.existsSync(contentRoot)) {
+    throw new Error(`Can't serve content from non-existent directory 
'${contentRoot}'.`);
+  }
+
+  const serveAsStaticContent = serveStatic(contentRoot);
+
+  const server = http.createServer((req, res) => {
+    // bare git repos
+    if (req.url?.split("/")[1].endsWith(".git")) {
+      log(`Received request for '${req.url}'...`);
+      log(`Serving as "smart" HTTP for Git.`);
+      serveAsGitSmartHttp(req, res, contentRoot);
+    }
+    // static content
+    else {
+      log(`Received request for '${req.url}'...`);
+      log("Serving as static content.");
+      serveAsStaticContent(req, res, finalhandler(req, res));
+    }
+  });
+
+  server.listen(port, () => {
+    printSummary(contentRoot, port, log);
+  });
+
+  return server;
+}
+
+function serveAsGitSmartHttp(req: http.IncomingMessage, res: 
http.ServerResponse, contentRoot: string): void {
+  const gitHttpBackend = spawn("git", ["http-backend"], { env: 
getEnvForGitHttpBackend(req, contentRoot) });
+
+  req.pipe(gitHttpBackend.stdin);
+
+  const buffers: BufferState = {
+    header: [],
+    body: [],
+    completedHeader: false,
+  };
+
+  gitHttpBackend.stdout.on("data", (chunk: Buffer) => writeData(chunk, 
buffers, res));
+  gitHttpBackend.on("close", () => {
+    res.end();
+  });
+}
+
+function getEnvForGitHttpBackend(req: http.IncomingMessage, contentRoot: 
string): NodeJS.ProcessEnv {
+  const [pathname, queryString] = (req.url || "").split("?");
+  const envVars: NodeJS.ProcessEnv = {};
+
+  for (const header in req.headers) {
+    const name = header.toUpperCase().replace(/-/g, "_");
+    if (gitHttpBackendVariableNames.includes(name)) {
+      envVars[name] = req.headers[header] as string;
+    }
+  }
+
+  envVars["GIT_PROJECT_ROOT"] = contentRoot;
+  envVars["PATH_TRANSLATED"] = contentRoot + pathname;
+  envVars["PATH_INFO"] = pathname;

Review Comment:
   Potential path traversal vulnerability in the Git HTTP backend environment 
variables. The code constructs PATH_TRANSLATED by directly concatenating 
contentRoot with the pathname from the request URL without proper validation or 
sanitization. A malicious request could potentially access files outside the 
intended contentRoot directory using path traversal sequences like "../".
   
   Consider sanitizing the pathname before constructing PATH_TRANSLATED, using 
path.resolve() and verifying that the resolved path is still within the 
contentRoot directory.
   ```suggestion
     const root = path.resolve(contentRoot);
     const safePathname = pathname || "/";
     const resolvedPath = path.resolve(root, "." + safePathname);
     const translatedPath =
       resolvedPath === root || resolvedPath.startsWith(root + path.sep) ? 
resolvedPath : root;
   
     envVars["GIT_PROJECT_ROOT"] = contentRoot;
     envVars["PATH_TRANSLATED"] = translatedPath;
     envVars["PATH_INFO"] = safePathname;
   ```



##########
packages/accelerator-git-http-server/bin.js:
##########
@@ -0,0 +1,22 @@
+#!/usr/bin/env node
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+require("./dist");

Review Comment:
   The bin.js file requires "./dist" but based on the TypeScript configuration 
and the structure of the source files, the compiled entry point should be 
"./dist/index.js" (not just "./dist"). The src/index.ts file is the CLI entry 
point that should be compiled to dist/index.js. 
   
   Verify that the bin.js correctly points to the compiled entry point. It 
should likely be `require("./dist/index");` instead of `require("./dist");`.
   ```suggestion
   require("./dist/index");
   ```



##########
packages/accelerator-git-http-server/src/server.ts:
##########
@@ -0,0 +1,193 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import * as http from "http";
+import * as path from "path";
+import * as fs from "fs";
+import { spawn } from "child_process";
+import serveStatic from "serve-static";
+import finalhandler from "finalhandler";
+
+export interface GitHttpServerConfig {
+  port: number;
+  contentRoot: string;
+  logPrefix?: string;
+}
+
+const gitHttpBackendVariableNames = [
+  "QUERY_STRING",
+  "REMOTE_USER",
+  "CONTENT_LENGTH",
+  "HTTP_CONTENT_ENCODING",
+  "REMOTE_USER",
+  "REMOTE_ADDR",
+  "GIT_COMMITTER_NAME",
+  "GIT_COMMITTER_EMAIL",
+  "CONTENT_TYPE",
+  "PATH_INFO",
+  "GIT_PROJECT_ROOT",
+  "PATH_TRANSLATED",
+  "SERVER_PROTOCOL",
+  "REQUEST_METHOD",
+  "GIT_HTTP_EXPORT_ALL",
+  "GIT_HTTP_MAX_REQUEST_BUFFER",
+];
+
+interface BufferState {
+  header: Buffer[];
+  body: Buffer[];
+  completedHeader: boolean;
+}
+
+/**
+ * Creates and starts a Git HTTP server
+ * @param options Server configuration options
+ * @returns HTTP server instance
+ */
+export function startGitHttpServer(options: GitHttpServerConfig): http.Server {
+  const { port, contentRoot, logPrefix = "git-repo-http-dev-server" } = 
options;
+
+  const log = (message: string) => console.log(`[${logPrefix}] ${message}`);
+
+  if (!fs.existsSync(contentRoot)) {
+    throw new Error(`Can't serve content from non-existent directory 
'${contentRoot}'.`);
+  }
+
+  const serveAsStaticContent = serveStatic(contentRoot);
+
+  const server = http.createServer((req, res) => {
+    // bare git repos
+    if (req.url?.split("/")[1].endsWith(".git")) {
+      log(`Received request for '${req.url}'...`);
+      log(`Serving as "smart" HTTP for Git.`);
+      serveAsGitSmartHttp(req, res, contentRoot);
+    }
+    // static content
+    else {
+      log(`Received request for '${req.url}'...`);
+      log("Serving as static content.");
+      serveAsStaticContent(req, res, finalhandler(req, res));
+    }
+  });
+
+  server.listen(port, () => {
+    printSummary(contentRoot, port, log);
+  });
+
+  return server;
+}
+
+function serveAsGitSmartHttp(req: http.IncomingMessage, res: 
http.ServerResponse, contentRoot: string): void {
+  const gitHttpBackend = spawn("git", ["http-backend"], { env: 
getEnvForGitHttpBackend(req, contentRoot) });
+
+  req.pipe(gitHttpBackend.stdin);
+
+  const buffers: BufferState = {
+    header: [],
+    body: [],
+    completedHeader: false,
+  };
+
+  gitHttpBackend.stdout.on("data", (chunk: Buffer) => writeData(chunk, 
buffers, res));
+  gitHttpBackend.on("close", () => {
+    res.end();
+  });
+}
+
+function getEnvForGitHttpBackend(req: http.IncomingMessage, contentRoot: 
string): NodeJS.ProcessEnv {
+  const [pathname, queryString] = (req.url || "").split("?");
+  const envVars: NodeJS.ProcessEnv = {};
+
+  for (const header in req.headers) {
+    const name = header.toUpperCase().replace(/-/g, "_");
+    if (gitHttpBackendVariableNames.includes(name)) {
+      envVars[name] = req.headers[header] as string;
+    }
+  }
+
+  envVars["GIT_PROJECT_ROOT"] = contentRoot;
+  envVars["PATH_TRANSLATED"] = contentRoot + pathname;
+  envVars["PATH_INFO"] = pathname;
+  envVars["REQUEST_METHOD"] = req.method;
+  envVars["GIT_HTTP_EXPORT_ALL"] = "1";
+  envVars["QUERY_STRING"] = queryString;
+
+  return envVars;
+}
+
+function writeData(chunk: Buffer, buffers: BufferState, res: 
http.ServerResponse): void {
+  if (buffers.completedHeader) {
+    res.write(chunk);
+  } else {
+    buffers.completedHeader = readMaybeHeaderBuffer(chunk, buffers);
+    if (buffers.completedHeader) {
+      writeHeader(buffers.header, res);
+      writeBody(buffers.body, res);
+    }
+  }
+}
+
+function writeHeader(header: Buffer[], res: http.ServerResponse): void {
+  const headerLines = Buffer.concat(header).toString().split("\r\n");
+  for (const headerLine of headerLines) {
+    const headerSplit = headerLine.split(":");
+    const headerKey = headerSplit[0];
+    const headerVal = headerSplit[1];
+    if (headerKey && headerVal) {
+      res.setHeader(headerKey, headerVal);
+    }
+  }
+}
+
+function writeBody(body: Buffer[], res: http.ServerResponse): void {
+  body.forEach((b) => res.write(b));
+}
+
+function readMaybeHeaderBuffer(nextBuffer: Buffer, buffers: BufferState): 
boolean {
+  const length = Buffer.from("\r\n\r\n", "utf-8").length;
+  const offset = nextBuffer.indexOf("\r\n\r\n", 0, "utf-8");
+  if (offset <= 0) {

Review Comment:
   The condition `if (offset <= 0)` is incorrect. When indexOf returns -1 (not 
found), the condition should return false, which it does. However, when offset 
is 0 (header separator is at the very beginning), this is actually a valid case 
that represents an empty header section, and the function should process it. 
The condition should be `if (offset < 0)` instead of `if (offset <= 0)`.
   ```suggestion
     if (offset < 0) {
   ```



##########
packages/accelerator-git-http-server/src/server.ts:
##########
@@ -0,0 +1,193 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import * as http from "http";
+import * as path from "path";
+import * as fs from "fs";
+import { spawn } from "child_process";
+import serveStatic from "serve-static";
+import finalhandler from "finalhandler";
+
+export interface GitHttpServerConfig {
+  port: number;
+  contentRoot: string;
+  logPrefix?: string;
+}
+
+const gitHttpBackendVariableNames = [
+  "QUERY_STRING",
+  "REMOTE_USER",
+  "CONTENT_LENGTH",
+  "HTTP_CONTENT_ENCODING",
+  "REMOTE_USER",
+  "REMOTE_ADDR",
+  "GIT_COMMITTER_NAME",
+  "GIT_COMMITTER_EMAIL",
+  "CONTENT_TYPE",
+  "PATH_INFO",
+  "GIT_PROJECT_ROOT",
+  "PATH_TRANSLATED",
+  "SERVER_PROTOCOL",
+  "REQUEST_METHOD",
+  "GIT_HTTP_EXPORT_ALL",
+  "GIT_HTTP_MAX_REQUEST_BUFFER",
+];
+
+interface BufferState {
+  header: Buffer[];
+  body: Buffer[];
+  completedHeader: boolean;
+}
+
+/**
+ * Creates and starts a Git HTTP server
+ * @param options Server configuration options
+ * @returns HTTP server instance
+ */
+export function startGitHttpServer(options: GitHttpServerConfig): http.Server {
+  const { port, contentRoot, logPrefix = "git-repo-http-dev-server" } = 
options;
+
+  const log = (message: string) => console.log(`[${logPrefix}] ${message}`);
+
+  if (!fs.existsSync(contentRoot)) {
+    throw new Error(`Can't serve content from non-existent directory 
'${contentRoot}'.`);
+  }
+
+  const serveAsStaticContent = serveStatic(contentRoot);
+
+  const server = http.createServer((req, res) => {
+    // bare git repos
+    if (req.url?.split("/")[1].endsWith(".git")) {
+      log(`Received request for '${req.url}'...`);
+      log(`Serving as "smart" HTTP for Git.`);
+      serveAsGitSmartHttp(req, res, contentRoot);
+    }
+    // static content
+    else {
+      log(`Received request for '${req.url}'...`);
+      log("Serving as static content.");
+      serveAsStaticContent(req, res, finalhandler(req, res));
+    }
+  });
+
+  server.listen(port, () => {
+    printSummary(contentRoot, port, log);
+  });
+
+  return server;
+}
+
+function serveAsGitSmartHttp(req: http.IncomingMessage, res: 
http.ServerResponse, contentRoot: string): void {
+  const gitHttpBackend = spawn("git", ["http-backend"], { env: 
getEnvForGitHttpBackend(req, contentRoot) });
+
+  req.pipe(gitHttpBackend.stdin);
+
+  const buffers: BufferState = {
+    header: [],
+    body: [],
+    completedHeader: false,
+  };
+
+  gitHttpBackend.stdout.on("data", (chunk: Buffer) => writeData(chunk, 
buffers, res));
+  gitHttpBackend.on("close", () => {
+    res.end();
+  });
+}
+
+function getEnvForGitHttpBackend(req: http.IncomingMessage, contentRoot: 
string): NodeJS.ProcessEnv {
+  const [pathname, queryString] = (req.url || "").split("?");
+  const envVars: NodeJS.ProcessEnv = {};
+
+  for (const header in req.headers) {
+    const name = header.toUpperCase().replace(/-/g, "_");
+    if (gitHttpBackendVariableNames.includes(name)) {
+      envVars[name] = req.headers[header] as string;

Review Comment:
   The request headers may contain array values (when multiple headers with the 
same name are sent), but the code casts them directly to string. The type of 
`req.headers[header]` can be `string | string[] | undefined`, and casting an 
array to string would result in a comma-separated value which may not be the 
intended behavior for Git's http-backend.
   
   Consider handling the array case explicitly, either by joining with 
appropriate separators or by taking the first value.
   ```suggestion
         const headerValue = req.headers[header];
         if (typeof headerValue === "string") {
           envVars[name] = headerValue;
         } else if (Array.isArray(headerValue) && headerValue.length > 0) {
           envVars[name] = headerValue[0];
         }
   ```



##########
packages/accelerator-git-http-server/README.md:
##########
@@ -0,0 +1,110 @@
+<!--
+   Licensed to the Apache Software Foundation (ASF) under one
+   or more contributor license agreements.  See the NOTICE file
+   distributed with this work for additional information
+   regarding copyright ownership.  The ASF licenses this file
+   to you under the Apache License, Version 2.0 (the
+   "License"); you may not use this file except in compliance
+   with the License.  You may obtain a copy of the License at
+     http://www.apache.org/licenses/LICENSE-2.0
+   Unless required by applicable law or agreed to in writing,
+   software distributed under the License is distributed on an
+   "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+   KIND, either express or implied.  See the License for the
+   specific language governing permissions and limitations
+   under the License.
+-->
+
+# @kie-tools/accelerator-git-http-server
+
+A reusable Git HTTP server for Accelerator packages. This package provides a 
simple HTTP server that can serve both Git repositories (using Git's "smart" 
HTTP protocol) and static content.
+
+## Features
+
+- **Git Smart HTTP Protocol**: Serves bare Git repositories over HTTP, 
allowing `git clone`, `git pull`, and `git push` operations
+- **Static Content Serving**: Serves static files and directories alongside 
Git repositories
+
+## Usage
+
+### As a Library
+
+```typescript
+import { startGitHttpServer } from "@kie-tools/accelerator-git-http-server";
+
+const server = startGitHttpServer({
+  port: 8080,
+  contentRoot: "./dist-dev",
+  logPrefix: "my-accelerator", // Optional
+});
+```
+
+### As a CLI Tool
+
+```bash
+kie-tools--accelerator-git-http-server [port] [content-root-path] 
[accelerator-name?]
+```
+
+Example:
+
+```bash
+kie-tools--accelerator-git-http-server 8080 ./dist-dev 
kie-sandbox-accelerator-quarkus-git-http-server
+```
+
+## Configuration Options
+
+### `StartServerOptions`

Review Comment:
   The README documentation refers to the configuration interface as 
"StartServerOptions" (line 55), but the actual TypeScript interface is named 
"GitHttpServerConfig" (line 27 in server.ts). This inconsistency should be 
corrected to match the actual implementation.
   ```suggestion
   ### `GitHttpServerConfig`
   ```



##########
packages/accelerator-git-http-server/src/server.ts:
##########
@@ -0,0 +1,193 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import * as http from "http";
+import * as path from "path";
+import * as fs from "fs";
+import { spawn } from "child_process";
+import serveStatic from "serve-static";
+import finalhandler from "finalhandler";
+
+export interface GitHttpServerConfig {
+  port: number;
+  contentRoot: string;
+  logPrefix?: string;
+}
+
+const gitHttpBackendVariableNames = [
+  "QUERY_STRING",
+  "REMOTE_USER",
+  "CONTENT_LENGTH",
+  "HTTP_CONTENT_ENCODING",
+  "REMOTE_USER",
+  "REMOTE_ADDR",
+  "GIT_COMMITTER_NAME",
+  "GIT_COMMITTER_EMAIL",
+  "CONTENT_TYPE",
+  "PATH_INFO",
+  "GIT_PROJECT_ROOT",
+  "PATH_TRANSLATED",
+  "SERVER_PROTOCOL",
+  "REQUEST_METHOD",
+  "GIT_HTTP_EXPORT_ALL",
+  "GIT_HTTP_MAX_REQUEST_BUFFER",
+];
+
+interface BufferState {
+  header: Buffer[];
+  body: Buffer[];
+  completedHeader: boolean;
+}
+
+/**
+ * Creates and starts a Git HTTP server
+ * @param options Server configuration options
+ * @returns HTTP server instance
+ */
+export function startGitHttpServer(options: GitHttpServerConfig): http.Server {
+  const { port, contentRoot, logPrefix = "git-repo-http-dev-server" } = 
options;
+
+  const log = (message: string) => console.log(`[${logPrefix}] ${message}`);
+
+  if (!fs.existsSync(contentRoot)) {
+    throw new Error(`Can't serve content from non-existent directory 
'${contentRoot}'.`);
+  }
+
+  const serveAsStaticContent = serveStatic(contentRoot);
+
+  const server = http.createServer((req, res) => {
+    // bare git repos
+    if (req.url?.split("/")[1].endsWith(".git")) {
+      log(`Received request for '${req.url}'...`);
+      log(`Serving as "smart" HTTP for Git.`);
+      serveAsGitSmartHttp(req, res, contentRoot);
+    }
+    // static content
+    else {
+      log(`Received request for '${req.url}'...`);
+      log("Serving as static content.");
+      serveAsStaticContent(req, res, finalhandler(req, res));
+    }
+  });
+
+  server.listen(port, () => {
+    printSummary(contentRoot, port, log);
+  });
+
+  return server;
+}
+
+function serveAsGitSmartHttp(req: http.IncomingMessage, res: 
http.ServerResponse, contentRoot: string): void {
+  const gitHttpBackend = spawn("git", ["http-backend"], { env: 
getEnvForGitHttpBackend(req, contentRoot) });
+
+  req.pipe(gitHttpBackend.stdin);
+
+  const buffers: BufferState = {
+    header: [],
+    body: [],
+    completedHeader: false,
+  };
+
+  gitHttpBackend.stdout.on("data", (chunk: Buffer) => writeData(chunk, 
buffers, res));
+  gitHttpBackend.on("close", () => {
+    res.end();
+  });
+}
+
+function getEnvForGitHttpBackend(req: http.IncomingMessage, contentRoot: 
string): NodeJS.ProcessEnv {
+  const [pathname, queryString] = (req.url || "").split("?");
+  const envVars: NodeJS.ProcessEnv = {};
+
+  for (const header in req.headers) {
+    const name = header.toUpperCase().replace(/-/g, "_");
+    if (gitHttpBackendVariableNames.includes(name)) {
+      envVars[name] = req.headers[header] as string;
+    }
+  }
+
+  envVars["GIT_PROJECT_ROOT"] = contentRoot;
+  envVars["PATH_TRANSLATED"] = contentRoot + pathname;
+  envVars["PATH_INFO"] = pathname;
+  envVars["REQUEST_METHOD"] = req.method;
+  envVars["GIT_HTTP_EXPORT_ALL"] = "1";
+  envVars["QUERY_STRING"] = queryString;
+
+  return envVars;
+}
+
+function writeData(chunk: Buffer, buffers: BufferState, res: 
http.ServerResponse): void {
+  if (buffers.completedHeader) {
+    res.write(chunk);
+  } else {
+    buffers.completedHeader = readMaybeHeaderBuffer(chunk, buffers);
+    if (buffers.completedHeader) {
+      writeHeader(buffers.header, res);
+      writeBody(buffers.body, res);
+    }
+  }
+}
+
+function writeHeader(header: Buffer[], res: http.ServerResponse): void {
+  const headerLines = Buffer.concat(header).toString().split("\r\n");
+  for (const headerLine of headerLines) {
+    const headerSplit = headerLine.split(":");
+    const headerKey = headerSplit[0];
+    const headerVal = headerSplit[1];

Review Comment:
   The path matching for headers could be more robust. When splitting headers 
on ":", if a header line is empty or malformed, the code could potentially try 
to set an empty header key. While the check `if (headerKey && headerVal)` 
prevents setting headers with falsy values, the code should also handle edge 
cases like header lines that don't contain a colon at all.
   
   Consider adding validation that headerSplit.length >= 2 before attempting to 
access indices.
   ```suggestion
       if (!headerLine) {
         continue;
       }
       const headerSplit = headerLine.split(":");
       if (headerSplit.length < 2) {
         continue;
       }
       const headerKey = headerSplit[0].trim();
       const headerVal = headerSplit.slice(1).join(":").trim();
   ```



##########
packages/accelerator-git-http-server/src/server.ts:
##########
@@ -0,0 +1,193 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import * as http from "http";
+import * as path from "path";
+import * as fs from "fs";
+import { spawn } from "child_process";
+import serveStatic from "serve-static";
+import finalhandler from "finalhandler";
+
+export interface GitHttpServerConfig {
+  port: number;
+  contentRoot: string;
+  logPrefix?: string;
+}
+
+const gitHttpBackendVariableNames = [
+  "QUERY_STRING",
+  "REMOTE_USER",
+  "CONTENT_LENGTH",
+  "HTTP_CONTENT_ENCODING",
+  "REMOTE_USER",
+  "REMOTE_ADDR",
+  "GIT_COMMITTER_NAME",
+  "GIT_COMMITTER_EMAIL",
+  "CONTENT_TYPE",
+  "PATH_INFO",
+  "GIT_PROJECT_ROOT",
+  "PATH_TRANSLATED",
+  "SERVER_PROTOCOL",
+  "REQUEST_METHOD",
+  "GIT_HTTP_EXPORT_ALL",
+  "GIT_HTTP_MAX_REQUEST_BUFFER",
+];
+
+interface BufferState {
+  header: Buffer[];
+  body: Buffer[];
+  completedHeader: boolean;
+}
+
+/**
+ * Creates and starts a Git HTTP server
+ * @param options Server configuration options
+ * @returns HTTP server instance
+ */
+export function startGitHttpServer(options: GitHttpServerConfig): http.Server {
+  const { port, contentRoot, logPrefix = "git-repo-http-dev-server" } = 
options;
+
+  const log = (message: string) => console.log(`[${logPrefix}] ${message}`);
+
+  if (!fs.existsSync(contentRoot)) {
+    throw new Error(`Can't serve content from non-existent directory 
'${contentRoot}'.`);
+  }
+
+  const serveAsStaticContent = serveStatic(contentRoot);
+
+  const server = http.createServer((req, res) => {
+    // bare git repos
+    if (req.url?.split("/")[1].endsWith(".git")) {
+      log(`Received request for '${req.url}'...`);
+      log(`Serving as "smart" HTTP for Git.`);
+      serveAsGitSmartHttp(req, res, contentRoot);
+    }
+    // static content
+    else {
+      log(`Received request for '${req.url}'...`);
+      log("Serving as static content.");
+      serveAsStaticContent(req, res, finalhandler(req, res));
+    }
+  });
+
+  server.listen(port, () => {
+    printSummary(contentRoot, port, log);
+  });
+
+  return server;
+}
+
+function serveAsGitSmartHttp(req: http.IncomingMessage, res: 
http.ServerResponse, contentRoot: string): void {
+  const gitHttpBackend = spawn("git", ["http-backend"], { env: 
getEnvForGitHttpBackend(req, contentRoot) });
+
+  req.pipe(gitHttpBackend.stdin);
+
+  const buffers: BufferState = {
+    header: [],
+    body: [],
+    completedHeader: false,
+  };
+
+  gitHttpBackend.stdout.on("data", (chunk: Buffer) => writeData(chunk, 
buffers, res));
+  gitHttpBackend.on("close", () => {
+    res.end();
+  });
+}
+
+function getEnvForGitHttpBackend(req: http.IncomingMessage, contentRoot: 
string): NodeJS.ProcessEnv {
+  const [pathname, queryString] = (req.url || "").split("?");
+  const envVars: NodeJS.ProcessEnv = {};
+
+  for (const header in req.headers) {
+    const name = header.toUpperCase().replace(/-/g, "_");
+    if (gitHttpBackendVariableNames.includes(name)) {
+      envVars[name] = req.headers[header] as string;
+    }
+  }
+
+  envVars["GIT_PROJECT_ROOT"] = contentRoot;
+  envVars["PATH_TRANSLATED"] = contentRoot + pathname;
+  envVars["PATH_INFO"] = pathname;
+  envVars["REQUEST_METHOD"] = req.method;
+  envVars["GIT_HTTP_EXPORT_ALL"] = "1";
+  envVars["QUERY_STRING"] = queryString;
+
+  return envVars;
+}
+
+function writeData(chunk: Buffer, buffers: BufferState, res: 
http.ServerResponse): void {
+  if (buffers.completedHeader) {
+    res.write(chunk);
+  } else {
+    buffers.completedHeader = readMaybeHeaderBuffer(chunk, buffers);
+    if (buffers.completedHeader) {
+      writeHeader(buffers.header, res);
+      writeBody(buffers.body, res);
+    }
+  }
+}
+
+function writeHeader(header: Buffer[], res: http.ServerResponse): void {
+  const headerLines = Buffer.concat(header).toString().split("\r\n");
+  for (const headerLine of headerLines) {
+    const headerSplit = headerLine.split(":");
+    const headerKey = headerSplit[0];
+    const headerVal = headerSplit[1];

Review Comment:
   The header value needs to be trimmed before being set. HTTP headers can have 
leading whitespace after the colon (e.g., "Content-Type: text/html"), and the 
current code would include that whitespace in the header value.
   
   Add `.trim()` to the headerVal before checking and setting it: `const 
headerVal = headerSplit[1]?.trim();`
   ```suggestion
       const headerVal = headerSplit[1]?.trim();
   ```



##########
packages/accelerator-git-http-server/src/server.ts:
##########
@@ -0,0 +1,193 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import * as http from "http";
+import * as path from "path";
+import * as fs from "fs";
+import { spawn } from "child_process";
+import serveStatic from "serve-static";
+import finalhandler from "finalhandler";
+
+export interface GitHttpServerConfig {
+  port: number;
+  contentRoot: string;
+  logPrefix?: string;
+}
+
+const gitHttpBackendVariableNames = [
+  "QUERY_STRING",
+  "REMOTE_USER",
+  "CONTENT_LENGTH",
+  "HTTP_CONTENT_ENCODING",
+  "REMOTE_USER",
+  "REMOTE_ADDR",
+  "GIT_COMMITTER_NAME",
+  "GIT_COMMITTER_EMAIL",
+  "CONTENT_TYPE",
+  "PATH_INFO",
+  "GIT_PROJECT_ROOT",
+  "PATH_TRANSLATED",
+  "SERVER_PROTOCOL",
+  "REQUEST_METHOD",
+  "GIT_HTTP_EXPORT_ALL",
+  "GIT_HTTP_MAX_REQUEST_BUFFER",
+];

Review Comment:
   The "REMOTE_USER" environment variable appears twice in the 
gitHttpBackendVariableNames array (lines 35 and 38). This is a duplication that 
should be removed.



##########
packages/accelerator-git-http-server/src/server.ts:
##########
@@ -0,0 +1,193 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import * as http from "http";
+import * as path from "path";
+import * as fs from "fs";
+import { spawn } from "child_process";
+import serveStatic from "serve-static";
+import finalhandler from "finalhandler";
+
+export interface GitHttpServerConfig {
+  port: number;
+  contentRoot: string;
+  logPrefix?: string;
+}
+
+const gitHttpBackendVariableNames = [
+  "QUERY_STRING",
+  "REMOTE_USER",
+  "CONTENT_LENGTH",
+  "HTTP_CONTENT_ENCODING",
+  "REMOTE_USER",
+  "REMOTE_ADDR",
+  "GIT_COMMITTER_NAME",
+  "GIT_COMMITTER_EMAIL",
+  "CONTENT_TYPE",
+  "PATH_INFO",
+  "GIT_PROJECT_ROOT",
+  "PATH_TRANSLATED",
+  "SERVER_PROTOCOL",
+  "REQUEST_METHOD",
+  "GIT_HTTP_EXPORT_ALL",
+  "GIT_HTTP_MAX_REQUEST_BUFFER",
+];
+
+interface BufferState {
+  header: Buffer[];
+  body: Buffer[];
+  completedHeader: boolean;
+}
+
+/**
+ * Creates and starts a Git HTTP server
+ * @param options Server configuration options
+ * @returns HTTP server instance
+ */
+export function startGitHttpServer(options: GitHttpServerConfig): http.Server {
+  const { port, contentRoot, logPrefix = "git-repo-http-dev-server" } = 
options;
+
+  const log = (message: string) => console.log(`[${logPrefix}] ${message}`);
+
+  if (!fs.existsSync(contentRoot)) {
+    throw new Error(`Can't serve content from non-existent directory 
'${contentRoot}'.`);
+  }
+
+  const serveAsStaticContent = serveStatic(contentRoot);
+
+  const server = http.createServer((req, res) => {
+    // bare git repos
+    if (req.url?.split("/")[1].endsWith(".git")) {
+      log(`Received request for '${req.url}'...`);
+      log(`Serving as "smart" HTTP for Git.`);
+      serveAsGitSmartHttp(req, res, contentRoot);
+    }
+    // static content
+    else {
+      log(`Received request for '${req.url}'...`);
+      log("Serving as static content.");
+      serveAsStaticContent(req, res, finalhandler(req, res));
+    }
+  });
+
+  server.listen(port, () => {
+    printSummary(contentRoot, port, log);
+  });
+
+  return server;
+}
+
+function serveAsGitSmartHttp(req: http.IncomingMessage, res: 
http.ServerResponse, contentRoot: string): void {
+  const gitHttpBackend = spawn("git", ["http-backend"], { env: 
getEnvForGitHttpBackend(req, contentRoot) });
+
+  req.pipe(gitHttpBackend.stdin);
+
+  const buffers: BufferState = {
+    header: [],
+    body: [],
+    completedHeader: false,
+  };
+
+  gitHttpBackend.stdout.on("data", (chunk: Buffer) => writeData(chunk, 
buffers, res));
+  gitHttpBackend.on("close", () => {
+    res.end();

Review Comment:
   Missing error handling for the spawned git process. If the git http-backend 
process fails (e.g., git is not installed, or the process crashes), there is no 
error handler attached to the spawned process. This could leave the HTTP 
response hanging or cause unhandled errors.
   
   Add error handlers for gitHttpBackend.on('error', ...) and 
gitHttpBackend.stderr.on('data', ...) to properly handle and log any errors 
from the git process.
   ```suggestion
   
     gitHttpBackend.stderr.on("data", (chunk: Buffer) => {
       // Log stderr from git http-backend for diagnostics
       console.error("[git-http-backend] stderr:", chunk.toString ? 
chunk.toString() : chunk);
     });
   
     gitHttpBackend.on("error", (err: Error) => {
       console.error("Failed to run git http-backend:", err);
       if (!res.headersSent) {
         res.statusCode = 500;
         res.setHeader("Content-Type", "text/plain");
         res.end("Internal Server Error");
       } else {
         res.end();
       }
     });
   
     gitHttpBackend.on("close", () => {
       if (!res.writableEnded) {
         res.end();
       }
   ```



##########
packages/accelerator-git-http-server/src/server.ts:
##########
@@ -0,0 +1,193 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import * as http from "http";
+import * as path from "path";
+import * as fs from "fs";
+import { spawn } from "child_process";
+import serveStatic from "serve-static";
+import finalhandler from "finalhandler";
+
+export interface GitHttpServerConfig {
+  port: number;
+  contentRoot: string;
+  logPrefix?: string;
+}
+
+const gitHttpBackendVariableNames = [
+  "QUERY_STRING",
+  "REMOTE_USER",
+  "CONTENT_LENGTH",
+  "HTTP_CONTENT_ENCODING",
+  "REMOTE_USER",
+  "REMOTE_ADDR",
+  "GIT_COMMITTER_NAME",
+  "GIT_COMMITTER_EMAIL",
+  "CONTENT_TYPE",
+  "PATH_INFO",
+  "GIT_PROJECT_ROOT",
+  "PATH_TRANSLATED",
+  "SERVER_PROTOCOL",
+  "REQUEST_METHOD",
+  "GIT_HTTP_EXPORT_ALL",
+  "GIT_HTTP_MAX_REQUEST_BUFFER",
+];
+
+interface BufferState {
+  header: Buffer[];
+  body: Buffer[];
+  completedHeader: boolean;
+}
+
+/**
+ * Creates and starts a Git HTTP server
+ * @param options Server configuration options
+ * @returns HTTP server instance
+ */
+export function startGitHttpServer(options: GitHttpServerConfig): http.Server {
+  const { port, contentRoot, logPrefix = "git-repo-http-dev-server" } = 
options;
+
+  const log = (message: string) => console.log(`[${logPrefix}] ${message}`);
+
+  if (!fs.existsSync(contentRoot)) {
+    throw new Error(`Can't serve content from non-existent directory 
'${contentRoot}'.`);
+  }
+
+  const serveAsStaticContent = serveStatic(contentRoot);
+
+  const server = http.createServer((req, res) => {
+    // bare git repos
+    if (req.url?.split("/")[1].endsWith(".git")) {

Review Comment:
   The logic to determine if a request is for a Git repository may not be 
robust. The condition `req.url?.split("/")[1].endsWith(".git")` only checks if 
the second path segment ends with ".git", but Git requests can include nested 
paths like `/repo.git/info/refs` or `/repo.git/git-upload-pack`. The current 
logic would fail for URLs like `/repo.git` (without trailing slash) or 
`/subdir/repo.git/...` (nested paths).
   
   Consider using a more robust check that looks for `.git` in any path 
segment, not just the second one. For example: 
`req.url?.split("/").some(segment => segment.endsWith(".git"))` or check if the 
pathname contains `.git/` or ends with `.git`.
   ```suggestion
       if (req.url?.split("/").some((segment) => segment.endsWith(".git"))) {
   ```



##########
packages/accelerator-git-http-server/README.md:
##########
@@ -0,0 +1,110 @@
+<!--
+   Licensed to the Apache Software Foundation (ASF) under one
+   or more contributor license agreements.  See the NOTICE file
+   distributed with this work for additional information
+   regarding copyright ownership.  The ASF licenses this file
+   to you under the Apache License, Version 2.0 (the
+   "License"); you may not use this file except in compliance
+   with the License.  You may obtain a copy of the License at
+     http://www.apache.org/licenses/LICENSE-2.0
+   Unless required by applicable law or agreed to in writing,
+   software distributed under the License is distributed on an
+   "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+   KIND, either express or implied.  See the License for the
+   specific language governing permissions and limitations
+   under the License.
+-->
+
+# @kie-tools/accelerator-git-http-server
+
+A reusable Git HTTP server for Accelerator packages. This package provides a 
simple HTTP server that can serve both Git repositories (using Git's "smart" 
HTTP protocol) and static content.
+
+## Features
+
+- **Git Smart HTTP Protocol**: Serves bare Git repositories over HTTP, 
allowing `git clone`, `git pull`, and `git push` operations
+- **Static Content Serving**: Serves static files and directories alongside 
Git repositories
+
+## Usage
+
+### As a Library
+
+```typescript
+import { startGitHttpServer } from "@kie-tools/accelerator-git-http-server";
+
+const server = startGitHttpServer({
+  port: 8080,
+  contentRoot: "./dist-dev",
+  logPrefix: "my-accelerator", // Optional
+});
+```
+
+### As a CLI Tool
+
+```bash
+kie-tools--accelerator-git-http-server [port] [content-root-path] 
[accelerator-name?]
+```
+
+Example:
+
+```bash
+kie-tools--accelerator-git-http-server 8080 ./dist-dev 
kie-sandbox-accelerator-quarkus-git-http-server
+```
+
+## Configuration Options
+
+### `StartServerOptions`
+
+| Option        | Type     | Required | Description                            
                       |
+| ------------- | -------- | -------- | 
------------------------------------------------------------- |
+| `port`        | `number` | Yes      | Port number for the HTTP server        
                       |
+| `contentRoot` | `string` | Yes      | Root directory containing Git 
repositories and static content |
+| `logPrefix`   | `string` | No       | Custom log prefix (defaults to 
"git-repo-http-dev-server")    |
+
+## Expected directory structure
+
+The `contentRoot` directory should contain:
+
+- **Bare Git repositories**: Directories ending with `.git` will be served 
using Git's smart HTTP protocol
+- **Static content**: All other directories and files will be served as static 
content
+
+Example structure:
+
+```
+dist-dev/
+├── my-repo.git/          # Bare Git repository (accessible via git clone)
+├── static-files/         # Static content directory
+│   ├── index.html
+│   └── styles.css
+└── another-repo.git/     # Another bare Git repository
+```
+
+## How It Works
+
+1. **Git Repositories**: When a request is made to a path ending with `.git`, 
the server uses Git's `http-backend` to handle the request, enabling full Git 
operations over HTTP.

Review Comment:
   The README states that Git requests are determined by "a path ending with 
`.git`" (line 83), but the actual implementation checks if the second path 
segment ends with `.git` (line 76 in server.ts). This is a discrepancy between 
the documentation and implementation. The documentation should accurately 
reflect the actual routing logic.
   ```suggestion
   1. **Git Repositories**: When a request is made where the second path 
segment (typically the repository name, e.g. `/my-repo.git/...`) ends with 
`.git`, the server uses Git's `http-backend` to handle the request, enabling 
full Git operations over HTTP.
   ```



##########
packages/accelerator-git-http-server/src/server.ts:
##########
@@ -0,0 +1,193 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import * as http from "http";
+import * as path from "path";
+import * as fs from "fs";
+import { spawn } from "child_process";
+import serveStatic from "serve-static";
+import finalhandler from "finalhandler";
+
+export interface GitHttpServerConfig {
+  port: number;
+  contentRoot: string;
+  logPrefix?: string;
+}
+
+const gitHttpBackendVariableNames = [
+  "QUERY_STRING",
+  "REMOTE_USER",
+  "CONTENT_LENGTH",
+  "HTTP_CONTENT_ENCODING",
+  "REMOTE_USER",
+  "REMOTE_ADDR",
+  "GIT_COMMITTER_NAME",
+  "GIT_COMMITTER_EMAIL",
+  "CONTENT_TYPE",
+  "PATH_INFO",
+  "GIT_PROJECT_ROOT",
+  "PATH_TRANSLATED",
+  "SERVER_PROTOCOL",
+  "REQUEST_METHOD",
+  "GIT_HTTP_EXPORT_ALL",
+  "GIT_HTTP_MAX_REQUEST_BUFFER",
+];
+
+interface BufferState {
+  header: Buffer[];
+  body: Buffer[];
+  completedHeader: boolean;
+}
+
+/**
+ * Creates and starts a Git HTTP server
+ * @param options Server configuration options
+ * @returns HTTP server instance
+ */
+export function startGitHttpServer(options: GitHttpServerConfig): http.Server {
+  const { port, contentRoot, logPrefix = "git-repo-http-dev-server" } = 
options;
+
+  const log = (message: string) => console.log(`[${logPrefix}] ${message}`);
+
+  if (!fs.existsSync(contentRoot)) {
+    throw new Error(`Can't serve content from non-existent directory 
'${contentRoot}'.`);
+  }
+
+  const serveAsStaticContent = serveStatic(contentRoot);
+
+  const server = http.createServer((req, res) => {
+    // bare git repos
+    if (req.url?.split("/")[1].endsWith(".git")) {
+      log(`Received request for '${req.url}'...`);
+      log(`Serving as "smart" HTTP for Git.`);
+      serveAsGitSmartHttp(req, res, contentRoot);
+    }
+    // static content
+    else {
+      log(`Received request for '${req.url}'...`);
+      log("Serving as static content.");
+      serveAsStaticContent(req, res, finalhandler(req, res));
+    }
+  });
+
+  server.listen(port, () => {
+    printSummary(contentRoot, port, log);
+  });
+
+  return server;
+}
+
+function serveAsGitSmartHttp(req: http.IncomingMessage, res: 
http.ServerResponse, contentRoot: string): void {
+  const gitHttpBackend = spawn("git", ["http-backend"], { env: 
getEnvForGitHttpBackend(req, contentRoot) });
+
+  req.pipe(gitHttpBackend.stdin);
+
+  const buffers: BufferState = {
+    header: [],
+    body: [],
+    completedHeader: false,
+  };
+
+  gitHttpBackend.stdout.on("data", (chunk: Buffer) => writeData(chunk, 
buffers, res));
+  gitHttpBackend.on("close", () => {
+    res.end();
+  });
+}
+
+function getEnvForGitHttpBackend(req: http.IncomingMessage, contentRoot: 
string): NodeJS.ProcessEnv {
+  const [pathname, queryString] = (req.url || "").split("?");
+  const envVars: NodeJS.ProcessEnv = {};
+
+  for (const header in req.headers) {
+    const name = header.toUpperCase().replace(/-/g, "_");
+    if (gitHttpBackendVariableNames.includes(name)) {
+      envVars[name] = req.headers[header] as string;
+    }
+  }
+
+  envVars["GIT_PROJECT_ROOT"] = contentRoot;
+  envVars["PATH_TRANSLATED"] = contentRoot + pathname;
+  envVars["PATH_INFO"] = pathname;
+  envVars["REQUEST_METHOD"] = req.method;
+  envVars["GIT_HTTP_EXPORT_ALL"] = "1";
+  envVars["QUERY_STRING"] = queryString;
+
+  return envVars;
+}
+
+function writeData(chunk: Buffer, buffers: BufferState, res: 
http.ServerResponse): void {
+  if (buffers.completedHeader) {
+    res.write(chunk);
+  } else {
+    buffers.completedHeader = readMaybeHeaderBuffer(chunk, buffers);
+    if (buffers.completedHeader) {
+      writeHeader(buffers.header, res);
+      writeBody(buffers.body, res);
+    }
+  }
+}
+
+function writeHeader(header: Buffer[], res: http.ServerResponse): void {
+  const headerLines = Buffer.concat(header).toString().split("\r\n");
+  for (const headerLine of headerLines) {
+    const headerSplit = headerLine.split(":");
+    const headerKey = headerSplit[0];
+    const headerVal = headerSplit[1];

Review Comment:
   The header parsing logic may fail when header values contain colons. The 
code splits on ":" and only takes the first two parts (headerSplit[0] and 
headerSplit[1]), but HTTP header values can legitimately contain colons (e.g., 
dates, URLs). 
   
   Consider using split with a limit parameter: `headerLine.split(":", 2)` or 
using indexOf(":") to split only at the first colon and preserve the rest of 
the value.
   ```suggestion
       const colonIndex = headerLine.indexOf(":");
       if (colonIndex <= 0) {
         continue;
       }
       const headerKey = headerLine.substring(0, colonIndex).trim();
       const headerVal = headerLine.substring(colonIndex + 1).trimStart();
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to