Skip to content

Commit

Permalink
feat: implement info/refs slice
Browse files Browse the repository at this point in the history
Add `InfoRefs` slice implementation which returns
server metadata and supported command that server can handle.
Based on this response git client decide what command to execute next.

Add `GitResponseOutput` to format ASCI text as git response
with length prefixes and part-end symbols.

Add `main` entry point to debug Artipie git server on localhost.

Update integration test `GitITCase`: setup bare repo on storage,
updated `ls-remote` test, added correct assertiion for `ls-remote` test.

Ticket: #1
Ticket: #11
  • Loading branch information
g4s8 committed Nov 19, 2021
1 parent 093e6c5 commit 2f11c0c
Show file tree
Hide file tree
Showing 10 changed files with 450 additions and 111 deletions.
87 changes: 22 additions & 65 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,26 @@ SOFTWARE.
<parent>
<groupId>com.artipie</groupId>
<artifactId>ppom</artifactId>
<version>1.0.4</version>
<version>1.1.0</version>
</parent>
<artifactId>git-adapter</artifactId>
<version>1.0-SNAPSHOT</version>
<name>git-adapter</name>
<description>An Artipie adapter for git repositories</description>
<url>https://github.com/artipie/git-adapter</url>
<inceptionYear>2021</inceptionYear>
<developers>
<developer>
<id>g4s8</id>
<name>Kirill Che.</name>
<email>[email protected]</email>
<organization>Artipie</organization>
<organizationUrl>https://www.artipie.com</organizationUrl>
<roles>
<role>maintainer</role>
</roles>
</developer>
</developers>
<licenses>
<license>
<name>MIT</name>
Expand Down Expand Up @@ -92,7 +104,6 @@ SOFTWARE.
<groupId>com.artipie</groupId>
<artifactId>vertx-server</artifactId>
<version>0.5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
Expand All @@ -103,83 +114,29 @@ SOFTWARE.
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<scope>test</scope>
<!-- <scope>test</scope> -->
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<scope>test</scope>
<!-- <scope>test</scope> -->
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.8.0-alpha2</version>
<scope>test</scope>
<!-- <scope>test</scope> -->
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M5</version>
<configuration combine.children="overwrite">
<failIfNoTests>false</failIfNoTests>
<failIfNoSpecifiedTests>false</failIfNoSpecifiedTests>
</configuration>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>com.jcabi</groupId>
<artifactId>jcabi-maven-plugin</artifactId>
<executions>
<execution>
<id>jcabi-versionalize-packages</id>
<phase>none</phase>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>qulice</id>
<build>
<plugins>
<plugin>
<groupId>com.qulice</groupId>
<artifactId>qulice-maven-plugin</artifactId>
<version>0.19.4</version>
<configuration>
<excludes combine.children="append">
<exclude>findbugs:.*</exclude>
<exclude>duplicatefinder:.*</exclude>
<exclude>xml:/src/it/settings.xml</exclude>
<exclude>dependencies:.*</exclude>
<exclude>checkstyle:.*/src/test/resources/.*</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
67 changes: 67 additions & 0 deletions src/main/java/com/artipie/git/GitResponseOutput.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* The MIT License (MIT) Copyright (c) 2020-2021 artipie.com
* https://github.com/artipie/git-adapter/LICENSE.txt
*/
package com.artipie.git;

import java.io.IOException;
import java.io.Writer;

/**
* Git response formatter writes binary data to
* wrapped {@link Writer} with correct identations and
* line prefixes.
* <p>
* Git response data consists of any number of part, each part
* ends with empty line (no data binary line).<br/>
* Git data line contains ASCI characters.<br/>
* Each data line is prefixed with 4-byte length of full line in hex ASCI
* format, e.g. text hello will be printed as
* {@code `000ahello\n`: `000a = hex(4 + len("hello") + 1)}<br/>
* Each part ends with empty data line: {@code 0000}.
* @since 1.0
*/
final class GitResponseOutput {

/**
* End part symbols.
*/
private static final char[] END_PART = new char[]{'0', '0', '0', '0'};

/**
* Output writer.
*/
private final Writer out;

/**
* New git output.
* @param writer Writer
*/
GitResponseOutput(final Writer writer) {
this.out = writer;
}

/**
* Push new line to output, format it as git data line.
* @param line ASCI text line
* @throws IOException On IO error
* @checkstyle MagicNumberCheck (10 lines)
*/
void pushLine(final String line) throws IOException {
final char[] src = line.toCharArray();
final char[] res = new char[src.length + 5];
final char[] len = String.format("%04x", res.length).toCharArray();
System.arraycopy(len, 0, res, 0, len.length);
System.arraycopy(src, 0, res, len.length, src.length);
res[res.length - 1] = '\n';
this.out.write(res);
}

/**
* Write end part symbol.
* @throws IOException Of IO error
*/
void endPart() throws IOException {
this.out.write(GitResponseOutput.END_PART);
}
}
55 changes: 43 additions & 12 deletions src/main/java/com/artipie/git/GitSlice.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,22 @@
package com.artipie.git;

import com.artipie.asto.Storage;
import com.artipie.asto.fs.FileStorage;
import com.artipie.http.Slice;
import com.artipie.http.rq.RequestLineFrom;
import com.artipie.http.rq.RqParams;
import com.artipie.http.rt.ByMethodsRule;
import com.artipie.http.rt.RtRule;
import com.artipie.http.rt.RtRulePath;
import com.artipie.http.rt.SliceRoute;
import com.artipie.http.slice.LoggingSlice;
import com.artipie.vertx.VertxSliceServer;
import com.jcabi.log.Logger;
import io.vertx.reactivex.core.Vertx;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Map.Entry;
import java.util.TreeSet;

/**
* Git main entry point.
Expand All @@ -21,6 +29,8 @@
* </p>
*
* @since 1.0
* @checkstyle MethodBodyCommentsCheck (500 lines)
* @checkstyle ClassDataAbstractionCouplingCheck (500 lines)
*/
public final class GitSlice extends Slice.Wrap {

Expand All @@ -32,13 +42,6 @@ public final class GitSlice extends Slice.Wrap {
public GitSlice(final Storage storage) {
super(
new SliceRoute(
new RtRulePath(
new RtRule.All(
ReceivePackSlice.RT_RULE,
ByMethodsRule.Standard.GET
),
new ReceivePackSlice.InfoRefSlice()
),
new RtRulePath(
new RtRule.All(
ReceivePackSlice.RT_RULE,
Expand All @@ -49,21 +52,49 @@ public GitSlice(final Storage storage) {
new RtRulePath(
new RtRule.All(
UploadPackSlice.RT_RULE,
ByMethodsRule.Standard.GET
ByMethodsRule.Standard.POST
),
new UploadPackSlice.InfoRefSlice()
new UploadPackSlice()
),
new RtRulePath(
new RtRule.All(
UploadPackSlice.RT_RULE,
ByMethodsRule.Standard.POST
new RtRule.ByPath("/info/refs"),
ByMethodsRule.Standard.GET
),
new UploadPackSlice()
new InfoRefsSlice(
"git/artipie",
new TreeSet<>(
Arrays.asList(
"ls-refs=unborn",
"fetch=shallow wait-for-done filter"
)
)
)
)
)
);
}

/**
* Main entry point for debugging with git.
* @param args First arg is a path to git dir
*/
public static void main(final String... args) {
final String repo;
if (args.length > 0) {
repo = args[0];
} else {
repo = "/tmp/artipie-git";
}
final VertxSliceServer server = new VertxSliceServer(
Vertx.vertx(),
new LoggingSlice(new GitSlice(new FileStorage(Path.of(repo)))),
8080
);
final int port = server.start();
Logger.info(GitSlice.class, "Artipie git server started at http://localhost:%d", port);
}

/**
* Routing rule by service name.
*
Expand Down
93 changes: 93 additions & 0 deletions src/main/java/com/artipie/git/InfoRefsSlice.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* The MIT License (MIT) Copyright (c) 2020-2021 artipie.com
* https://github.com/artipie/git-adapter/LICENSE.txt
*/
package com.artipie.git;

import com.artipie.asto.ArtipieIOException;
import com.artipie.asto.Content;
import com.artipie.http.ArtipieHttpException;
import com.artipie.http.Response;
import com.artipie.http.Slice;
import com.artipie.http.headers.ContentType;
import com.artipie.http.rq.RequestLineFrom;
import com.artipie.http.rq.RqParams;
import com.artipie.http.rs.RsStatus;
import com.artipie.http.rs.RsWithBody;
import com.artipie.http.rs.RsWithHeaders;
import com.artipie.http.rs.StandardRs;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.nio.ByteBuffer;
import java.util.Map.Entry;
import java.util.Set;
import org.reactivestreams.Publisher;

/**
* Slice to handle {@code /info/refs} - it's used
* to send metadata about git server, it shows supported commands
* services, versions, etc.
* @since 1.0
* @checkstyle ClassDataAbstractionCouplingCheck (500 lines)
* @checkstyle MethodBodyCommentsCheck (500 lines)
*/
final class InfoRefsSlice implements Slice {

/**
* Server agent name.
*/
private final String agent;

/**
* Supported commands.
*/
private final Set<String> commands;

/**
* New slice.
* @param agent Server agent name
* @param commands Supported commands with annotations
*/
InfoRefsSlice(final String agent, final Set<String> commands) {
this.agent = agent;
this.commands = commands;
}

@Override
public Response response(final String line, final Iterable<Entry<String, String>> headers,
final Publisher<ByteBuffer> body) {
final String service = new RqParams(new RequestLineFrom(line).uri()).value("service")
.orElseThrow(
() -> new ArtipieHttpException(
RsStatus.BAD_REQUEST,
"service query param required"
)
);
// this response is very small: <1K - it doesn't consume a lot of memory
// and it could be constructed in byte-array right here
final ByteArrayOutputStream baos = new ByteArrayOutputStream(256);
try (OutputStreamWriter osw = new OutputStreamWriter(baos)) {
final GitResponseOutput gwr = new GitResponseOutput(osw);
gwr.pushLine(String.format("# service=%s", service));
gwr.endPart();
gwr.pushLine("version 2");
gwr.pushLine(String.format("agent=%s", this.agent));
for (final String cmd : this.commands) {
gwr.pushLine(cmd);
}
gwr.pushLine("server-option");
gwr.pushLine("object-format=sha1");
gwr.endPart();
} catch (final IOException iex) {
throw new ArtipieHttpException(RsStatus.INTERNAL_ERROR, new ArtipieIOException(iex));
}
return new RsWithBody(
new RsWithHeaders(
StandardRs.OK,
new ContentType("application/x-git-upload-pack-advertisement")
),
new Content.From(baos.toByteArray())
);
}
}
Loading

0 comments on commit 2f11c0c

Please sign in to comment.