From e1f4070d86cc42e51351394dc126ed96d56e0acb Mon Sep 17 00:00:00 2001 From: Claus Ibsen Date: Fri, 23 Oct 2026 22:53:36 +0200 Subject: [PATCH 1/2] CAMEL-25983: camel-github2 - getCommitFile reads the file of the commit Backport of the camel-github2 part of #28373 to camel-4.22.x. Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: Claus Ibsen --- .../camel/catalog/components/github2.json | 9 +- .../camel/catalog/docs/github2-component.adoc | 6 +- .../camel/component/github2/github2.json | 9 +- .../src/main/docs/github2-component.adoc | 6 +- .../component/github2/GitHub2Constants.java | 9 +- .../component/github2/GitHub2Endpoint.java | 2 +- .../producer/GetCommitFileProducer.java | 73 +++++--- .../producer/GetCommitFileProducerTest.java | 161 ++++++++++++++++++ .../dsl/GitHub2EndpointBuilderFactory.java | 21 ++- 9 files changed, 254 insertions(+), 42 deletions(-) create mode 100644 components/camel-github2/src/test/java/org/apache/camel/component/github2/producer/GetCommitFileProducerTest.java diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/github2.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/github2.json index 3dea9be88ac19..4479b5dd39ef1 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/github2.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/github2.json @@ -39,9 +39,10 @@ "CamelGitHubIssueTitle": { "index": 3, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The title of the issue", "constantName": "org.apache.camel.component.github2.GitHub2Constants#GITHUB_ISSUE_TITLE" }, "CamelGitHubCommitAuthor": { "index": 4, "kind": "header", "displayName": "", "group": "consumer", "label": "consumer", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The commit author", "constantName": "org.apache.camel.component.github2.GitHub2Constants#GITHUB_COMMIT_AUTHOR" }, "CamelGitHubCommitCommitter": { "index": 5, "kind": "header", "displayName": "", "group": "consumer", "label": "consumer", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The committer name", "constantName": "org.apache.camel.component.github2.GitHub2Constants#GITHUB_COMMIT_COMMITTER" }, - "CamelGitHubCommitSha": { "index": 6, "kind": "header", "displayName": "", "group": "consumer", "label": "consumer", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The commit sha", "constantName": "org.apache.camel.component.github2.GitHub2Constants#GITHUB_COMMIT_SHA" }, - "CamelGitHubCommitUrl": { "index": 7, "kind": "header", "displayName": "", "group": "consumer", "label": "consumer", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The commit URL", "constantName": "org.apache.camel.component.github2.GitHub2Constants#GITHUB_COMMIT_URL" }, - "CamelGitHubEventPayload": { "index": 8, "kind": "header", "displayName": "", "group": "consumer", "label": "consumer", "required": false, "javaType": "org.kohsuke.github.GHEventPayload", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The event payload", "constantName": "org.apache.camel.component.github2.GitHub2Constants#GITHUB_EVENT_PAYLOAD" } + "CamelGitHubCommitSha": { "index": 6, "kind": "header", "displayName": "", "group": "common", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The commit sha. The getCommitFile producer reads the file at this commit (a commit id of 40 hexadecimal characters).", "constantName": "org.apache.camel.component.github2.GitHub2Constants#GITHUB_COMMIT_SHA" }, + "CamelGitHubFilePath": { "index": 7, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The path of the file in the repository for the getCommitFile producer. If not set, the body is the path.", "constantName": "org.apache.camel.component.github2.GitHub2Constants#GITHUB_FILE_PATH" }, + "CamelGitHubCommitUrl": { "index": 8, "kind": "header", "displayName": "", "group": "consumer", "label": "consumer", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The commit URL", "constantName": "org.apache.camel.component.github2.GitHub2Constants#GITHUB_COMMIT_URL" }, + "CamelGitHubEventPayload": { "index": 9, "kind": "header", "displayName": "", "group": "consumer", "label": "consumer", "required": false, "javaType": "org.kohsuke.github.GHEventPayload", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The event payload", "constantName": "org.apache.camel.component.github2.GitHub2Constants#GITHUB_EVENT_PAYLOAD" } }, "properties": { "type": { "index": 0, "kind": "path", "displayName": "Type", "group": "common", "label": "", "required": true, "type": "enum", "javaType": "org.apache.camel.component.github2.GitHub2Type", "enum": [ "CLOSEPULLREQUEST", "PULLREQUESTCOMMENT", "COMMIT", "PULLREQUEST", "TAG", "PULLREQUESTSTATE", "PULLREQUESTFILES", "GETCOMMITFILE", "CREATEISSUE", "EVENT" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "What git operation to execute" }, @@ -56,7 +57,7 @@ "exceptionHandler": { "index": 9, "kind": "parameter", "displayName": "Exception Handler", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "object", "javaType": "org.apache.camel.spi.ExceptionHandler", "optionalPrefix": "consumer.", "deprecated": false, "autowired": false, "secret": false, "description": "To let the consumer use a custom ExceptionHandler. Notice if the option bridgeErrorHandler is enabled then this option is not in use. By default the consumer will deal with exceptions, that will be logged at WARN or ERROR level and ignored." }, "exchangePattern": { "index": 10, "kind": "parameter", "displayName": "Exchange Pattern", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "enum", "javaType": "org.apache.camel.ExchangePattern", "enum": [ "InOnly", "InOut" ], "deprecated": false, "autowired": false, "secret": false, "description": "Sets the exchange pattern when the consumer creates an exchange." }, "pollStrategy": { "index": 11, "kind": "parameter", "displayName": "Poll Strategy", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "object", "javaType": "org.apache.camel.spi.PollingConsumerPollStrategy", "deprecated": false, "autowired": false, "secret": false, "description": "A pluggable org.apache.camel.PollingConsumerPollingStrategy allowing you to provide your custom implementation to control error handling usually occurred during the poll operation before an Exchange have been created and being routed in Camel." }, - "encoding": { "index": 12, "kind": "parameter", "displayName": "Encoding", "group": "producer", "label": "producer", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "To use the given encoding when getting a git commit file" }, + "encoding": { "index": 12, "kind": "parameter", "displayName": "Encoding", "group": "producer", "label": "producer", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The charset of the content of the file for the getCommitFile producer (the default charset of the JVM if not set)" }, "state": { "index": 13, "kind": "parameter", "displayName": "State", "group": "producer", "label": "producer", "required": false, "type": "enum", "javaType": "java.lang.String", "enum": [ "error", "failure", "pending", "success" ], "deprecated": false, "autowired": false, "secret": false, "description": "To set git commit status state" }, "targetUrl": { "index": 14, "kind": "parameter", "displayName": "Target Url", "group": "producer", "label": "producer", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "To set git commit status target url" }, "lazyStartProducer": { "index": 15, "kind": "parameter", "displayName": "Lazy Start Producer", "group": "producer (advanced)", "label": "producer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a producer may otherwise fail during starting and cause the route to fail being started. By deferring this startup to be lazy then the startup failure can be handled during routing messages via Camel's routing error handlers. Beware that when the first message is processed then creating and starting the producer may take a little time and prolong the total processing time of the processing." }, diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/github2-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/github2-component.adoc index 41b25e845dc0a..dc7d529872da8 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/github2-component.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/github2-component.adoc @@ -153,9 +153,9 @@ Sets the commit status on a pull request. |pullRequestFiles |none | - `CamelGitHubPullRequest` (integer) (REQUIRED): Pull request number. Returns the list of files changed in a pull request. -|getCommitFile |none | - `CamelGitHubCommitSha` (String) (REQUIRED): Commit SHA. -- `CamelGitHubFilePath` (String) (REQUIRED): File path. -Returns the content of a file at a specific commit. +|getCommitFile |String (the path of the file, if the `CamelGitHubFilePath` header is not set) | - `CamelGitHubCommitSha` (String) (REQUIRED): The commit id (40 hexadecimal characters; a branch or tag name is not accepted). +- `CamelGitHubFilePath` (String): The path of the file in the repository, such as `docs/README.md`. If not set, the body is the path. +Returns the content of the file in the tree of that commit, as a `String` (decoded with the `encoding` option, or the default charset of the JVM). |======================================================================= === Example: Consuming commits diff --git a/components/camel-github2/src/generated/resources/META-INF/org/apache/camel/component/github2/github2.json b/components/camel-github2/src/generated/resources/META-INF/org/apache/camel/component/github2/github2.json index 3dea9be88ac19..4479b5dd39ef1 100644 --- a/components/camel-github2/src/generated/resources/META-INF/org/apache/camel/component/github2/github2.json +++ b/components/camel-github2/src/generated/resources/META-INF/org/apache/camel/component/github2/github2.json @@ -39,9 +39,10 @@ "CamelGitHubIssueTitle": { "index": 3, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The title of the issue", "constantName": "org.apache.camel.component.github2.GitHub2Constants#GITHUB_ISSUE_TITLE" }, "CamelGitHubCommitAuthor": { "index": 4, "kind": "header", "displayName": "", "group": "consumer", "label": "consumer", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The commit author", "constantName": "org.apache.camel.component.github2.GitHub2Constants#GITHUB_COMMIT_AUTHOR" }, "CamelGitHubCommitCommitter": { "index": 5, "kind": "header", "displayName": "", "group": "consumer", "label": "consumer", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The committer name", "constantName": "org.apache.camel.component.github2.GitHub2Constants#GITHUB_COMMIT_COMMITTER" }, - "CamelGitHubCommitSha": { "index": 6, "kind": "header", "displayName": "", "group": "consumer", "label": "consumer", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The commit sha", "constantName": "org.apache.camel.component.github2.GitHub2Constants#GITHUB_COMMIT_SHA" }, - "CamelGitHubCommitUrl": { "index": 7, "kind": "header", "displayName": "", "group": "consumer", "label": "consumer", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The commit URL", "constantName": "org.apache.camel.component.github2.GitHub2Constants#GITHUB_COMMIT_URL" }, - "CamelGitHubEventPayload": { "index": 8, "kind": "header", "displayName": "", "group": "consumer", "label": "consumer", "required": false, "javaType": "org.kohsuke.github.GHEventPayload", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The event payload", "constantName": "org.apache.camel.component.github2.GitHub2Constants#GITHUB_EVENT_PAYLOAD" } + "CamelGitHubCommitSha": { "index": 6, "kind": "header", "displayName": "", "group": "common", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The commit sha. The getCommitFile producer reads the file at this commit (a commit id of 40 hexadecimal characters).", "constantName": "org.apache.camel.component.github2.GitHub2Constants#GITHUB_COMMIT_SHA" }, + "CamelGitHubFilePath": { "index": 7, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The path of the file in the repository for the getCommitFile producer. If not set, the body is the path.", "constantName": "org.apache.camel.component.github2.GitHub2Constants#GITHUB_FILE_PATH" }, + "CamelGitHubCommitUrl": { "index": 8, "kind": "header", "displayName": "", "group": "consumer", "label": "consumer", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The commit URL", "constantName": "org.apache.camel.component.github2.GitHub2Constants#GITHUB_COMMIT_URL" }, + "CamelGitHubEventPayload": { "index": 9, "kind": "header", "displayName": "", "group": "consumer", "label": "consumer", "required": false, "javaType": "org.kohsuke.github.GHEventPayload", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The event payload", "constantName": "org.apache.camel.component.github2.GitHub2Constants#GITHUB_EVENT_PAYLOAD" } }, "properties": { "type": { "index": 0, "kind": "path", "displayName": "Type", "group": "common", "label": "", "required": true, "type": "enum", "javaType": "org.apache.camel.component.github2.GitHub2Type", "enum": [ "CLOSEPULLREQUEST", "PULLREQUESTCOMMENT", "COMMIT", "PULLREQUEST", "TAG", "PULLREQUESTSTATE", "PULLREQUESTFILES", "GETCOMMITFILE", "CREATEISSUE", "EVENT" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "What git operation to execute" }, @@ -56,7 +57,7 @@ "exceptionHandler": { "index": 9, "kind": "parameter", "displayName": "Exception Handler", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "object", "javaType": "org.apache.camel.spi.ExceptionHandler", "optionalPrefix": "consumer.", "deprecated": false, "autowired": false, "secret": false, "description": "To let the consumer use a custom ExceptionHandler. Notice if the option bridgeErrorHandler is enabled then this option is not in use. By default the consumer will deal with exceptions, that will be logged at WARN or ERROR level and ignored." }, "exchangePattern": { "index": 10, "kind": "parameter", "displayName": "Exchange Pattern", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "enum", "javaType": "org.apache.camel.ExchangePattern", "enum": [ "InOnly", "InOut" ], "deprecated": false, "autowired": false, "secret": false, "description": "Sets the exchange pattern when the consumer creates an exchange." }, "pollStrategy": { "index": 11, "kind": "parameter", "displayName": "Poll Strategy", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "object", "javaType": "org.apache.camel.spi.PollingConsumerPollStrategy", "deprecated": false, "autowired": false, "secret": false, "description": "A pluggable org.apache.camel.PollingConsumerPollingStrategy allowing you to provide your custom implementation to control error handling usually occurred during the poll operation before an Exchange have been created and being routed in Camel." }, - "encoding": { "index": 12, "kind": "parameter", "displayName": "Encoding", "group": "producer", "label": "producer", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "To use the given encoding when getting a git commit file" }, + "encoding": { "index": 12, "kind": "parameter", "displayName": "Encoding", "group": "producer", "label": "producer", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The charset of the content of the file for the getCommitFile producer (the default charset of the JVM if not set)" }, "state": { "index": 13, "kind": "parameter", "displayName": "State", "group": "producer", "label": "producer", "required": false, "type": "enum", "javaType": "java.lang.String", "enum": [ "error", "failure", "pending", "success" ], "deprecated": false, "autowired": false, "secret": false, "description": "To set git commit status state" }, "targetUrl": { "index": 14, "kind": "parameter", "displayName": "Target Url", "group": "producer", "label": "producer", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "To set git commit status target url" }, "lazyStartProducer": { "index": 15, "kind": "parameter", "displayName": "Lazy Start Producer", "group": "producer (advanced)", "label": "producer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a producer may otherwise fail during starting and cause the route to fail being started. By deferring this startup to be lazy then the startup failure can be handled during routing messages via Camel's routing error handlers. Beware that when the first message is processed then creating and starting the producer may take a little time and prolong the total processing time of the processing." }, diff --git a/components/camel-github2/src/main/docs/github2-component.adoc b/components/camel-github2/src/main/docs/github2-component.adoc index 41b25e845dc0a..dc7d529872da8 100644 --- a/components/camel-github2/src/main/docs/github2-component.adoc +++ b/components/camel-github2/src/main/docs/github2-component.adoc @@ -153,9 +153,9 @@ Sets the commit status on a pull request. |pullRequestFiles |none | - `CamelGitHubPullRequest` (integer) (REQUIRED): Pull request number. Returns the list of files changed in a pull request. -|getCommitFile |none | - `CamelGitHubCommitSha` (String) (REQUIRED): Commit SHA. -- `CamelGitHubFilePath` (String) (REQUIRED): File path. -Returns the content of a file at a specific commit. +|getCommitFile |String (the path of the file, if the `CamelGitHubFilePath` header is not set) | - `CamelGitHubCommitSha` (String) (REQUIRED): The commit id (40 hexadecimal characters; a branch or tag name is not accepted). +- `CamelGitHubFilePath` (String): The path of the file in the repository, such as `docs/README.md`. If not set, the body is the path. +Returns the content of the file in the tree of that commit, as a `String` (decoded with the `encoding` option, or the default charset of the JVM). |======================================================================= === Example: Consuming commits diff --git a/components/camel-github2/src/main/java/org/apache/camel/component/github2/GitHub2Constants.java b/components/camel-github2/src/main/java/org/apache/camel/component/github2/GitHub2Constants.java index 140b48118a239..7560874cd6556 100644 --- a/components/camel-github2/src/main/java/org/apache/camel/component/github2/GitHub2Constants.java +++ b/components/camel-github2/src/main/java/org/apache/camel/component/github2/GitHub2Constants.java @@ -35,8 +35,15 @@ public interface GitHub2Constants { String GITHUB_COMMIT_AUTHOR = "CamelGitHubCommitAuthor"; @Metadata(label = "consumer", description = "The committer name", javaType = "String") String GITHUB_COMMIT_COMMITTER = "CamelGitHubCommitCommitter"; - @Metadata(label = "consumer", description = "The commit sha", javaType = "String") + @Metadata(description = "The commit sha. The getCommitFile producer reads the file at this commit (a commit id of" + + " 40 hexadecimal characters).", + javaType = "String") String GITHUB_COMMIT_SHA = "CamelGitHubCommitSha"; + @Metadata(label = "producer", + description = "The path of the file in the repository for the getCommitFile producer. If not set, the body" + + " is the path.", + javaType = "String") + String GITHUB_FILE_PATH = "CamelGitHubFilePath"; @Metadata(label = "consumer", description = "The commit URL", javaType = "String") String GITHUB_COMMIT_URL = "CamelGitHubCommitUrl"; @Metadata(label = "consumer", description = "The event payload", diff --git a/components/camel-github2/src/main/java/org/apache/camel/component/github2/GitHub2Endpoint.java b/components/camel-github2/src/main/java/org/apache/camel/component/github2/GitHub2Endpoint.java index 23d56fc73413f..452eb653a0a1d 100644 --- a/components/camel-github2/src/main/java/org/apache/camel/component/github2/GitHub2Endpoint.java +++ b/components/camel-github2/src/main/java/org/apache/camel/component/github2/GitHub2Endpoint.java @@ -252,7 +252,7 @@ public String getEncoding() { } /** - * To use the given encoding when getting a git commit file + * The charset of the content of the file for the getCommitFile producer (the default charset of the JVM if not set) */ public void setEncoding(String encoding) { this.encoding = encoding; diff --git a/components/camel-github2/src/main/java/org/apache/camel/component/github2/producer/GetCommitFileProducer.java b/components/camel-github2/src/main/java/org/apache/camel/component/github2/producer/GetCommitFileProducer.java index 6de27c410dadb..cd4826b15aade 100644 --- a/components/camel-github2/src/main/java/org/apache/camel/component/github2/producer/GetCommitFileProducer.java +++ b/components/camel-github2/src/main/java/org/apache/camel/component/github2/producer/GetCommitFileProducer.java @@ -16,22 +16,28 @@ */ package org.apache.camel.component.github2.producer; +import java.io.IOException; +import java.io.InputStream; import java.nio.charset.Charset; import java.util.Base64; +import java.util.regex.Pattern; import org.apache.camel.Exchange; +import org.apache.camel.InvalidPayloadException; import org.apache.camel.component.github2.GitHub2Constants; import org.apache.camel.component.github2.GitHub2Endpoint; import org.apache.camel.util.ObjectHelper; -import org.kohsuke.github.GHBlob; -import org.kohsuke.github.GHCommit; +import org.kohsuke.github.GHContent; /** - * Producer endpoint that gets a file from a commit. The endpoint requires the "CamelGitHubCommitSha" header, - * identifying the commit SHA. The message body should contain the file path within the repository. + * Producer endpoint that gets the content of a file at a commit. The "CamelGitHubCommitSha" header is the commit id (40 + * hexadecimal characters), and the "CamelGitHubFilePath" header, or else the message body, is the path of the file in + * the repository. The file is read from the tree of that commit, and the body is set to its content as text. */ public class GetCommitFileProducer extends AbstractGitHub2Producer { + private static final Pattern COMMIT_ID = Pattern.compile("[0-9a-fA-F]{40}"); + private final String encoding; public GetCommitFileProducer(GitHub2Endpoint endpoint) { @@ -43,35 +49,56 @@ public GetCommitFileProducer(GitHub2Endpoint endpoint) { public void process(Exchange exchange) throws Exception { String sha = exchange.getIn().getHeader(GitHub2Constants.GITHUB_COMMIT_SHA, String.class); if (ObjectHelper.isEmpty(sha)) { - sha = exchange.getIn().getBody(String.class); + throw new IllegalArgumentException( + "The getCommitFile operation needs the " + GitHub2Constants.GITHUB_COMMIT_SHA + + " header with the commit id"); } - - if (ObjectHelper.isEmpty(sha)) { - throw new IllegalArgumentException("Commit SHA must be specified"); + if (!COMMIT_ID.matcher(sha).matches()) { + throw new IllegalArgumentException( + "The " + GitHub2Constants.GITHUB_COMMIT_SHA + + " header must be a commit id of 40 hexadecimal characters, but was: " + + sha); } - GHCommit commit = getRepository().getCommit(sha); - GHBlob blob = null; + String path = exchange.getIn().getHeader(GitHub2Constants.GITHUB_FILE_PATH, String.class); + if (ObjectHelper.isEmpty(path)) { + path = exchange.getIn().getBody(String.class); + } + if (ObjectHelper.isEmpty(path)) { + throw new InvalidPayloadException(exchange, String.class, exchange.getIn()); + } - // Get the tree and find the file - // For simplicity, we get the blob SHA from the exchange body if it's a file path - String filePath = exchange.getIn().getBody(String.class); - if (filePath != null && !filePath.equals(sha)) { - // Try to get the file content from the commit - blob = getRepository().getBlob(sha); - } else { - blob = getRepository().getBlob(sha); + for (String segment : path.split("/")) { + if (".".equals(segment) || "..".equals(segment)) { + throw new IllegalArgumentException( + "The path of the file must be a path in the tree of the commit, without . or .. segments, but was: " + + path); + } } - String content = null; - if (blob != null) { - String encodedContent = blob.getContent(); - Charset charset = encoding != null ? Charset.forName(encoding) : Charset.defaultCharset(); - content = new String(Base64.getDecoder().decode(encodedContent), charset); + // the file at its path in the tree of the commit + GHContent file = getRepository().getFileContent(path, sha); + if (!file.isFile()) { + throw new IllegalArgumentException( + "The path " + path + " at commit " + sha + " is not a file but a " + file.getType()); } + Charset charset = encoding != null ? Charset.forName(encoding) : Charset.defaultCharset(); + String content = new String(readContent(file), charset); + // copy the header of in message to the out message exchange.getMessage().copyFrom(exchange.getIn()); exchange.getMessage().setBody(content); } + + private byte[] readContent(GHContent file) throws IOException { + if ("base64".equals(file.getEncoding())) { + // GitHub wraps the lines of the base64 content + return Base64.getMimeDecoder().decode(file.getEncodedContent()); + } + // the contents API gives no content for a file of more than 1 MB: read its blob + try (InputStream is = getRepository().getBlob(file.getSha()).read()) { + return is.readAllBytes(); + } + } } diff --git a/components/camel-github2/src/test/java/org/apache/camel/component/github2/producer/GetCommitFileProducerTest.java b/components/camel-github2/src/test/java/org/apache/camel/component/github2/producer/GetCommitFileProducerTest.java new file mode 100644 index 0000000000000..30c5145286443 --- /dev/null +++ b/components/camel-github2/src/test/java/org/apache/camel/component/github2/producer/GetCommitFileProducerTest.java @@ -0,0 +1,161 @@ +/* + * 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. + */ +package org.apache.camel.component.github2.producer; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Map; + +import org.apache.camel.BindToRegistry; +import org.apache.camel.CamelExecutionException; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.github2.GitHub2Constants; +import org.apache.camel.test.junit6.CamelTestSupport; +import org.junit.jupiter.api.Test; +import org.kohsuke.github.GHBlob; +import org.kohsuke.github.GHContent; +import org.kohsuke.github.GHRepository; +import org.kohsuke.github.GitHub; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * The getCommitFile producer reads the file at its path in the tree of the commit. + */ +class GetCommitFileProducerTest extends CamelTestSupport { + + private static final String SHA = "0123456789abcdef0123456789abcdef01234567"; + private static final String PATH = "docs/readme.md"; + // longer than the 76 characters of a line of MIME base64, so the encoded content has line breaks + private static final String TEXT = "The Open Source Integration Framework, with more than 300 connectors: æøå\n"; + + private final GHRepository repository = mock(GHRepository.class); + + @BindToRegistry(GitHub2Constants.GITHUB_CLIENT) + private final GitHub github = createGitHub(); + + @Override + protected RouteBuilder createRouteBuilder() { + return new RouteBuilder() { + @Override + public void configure() { + from("direct:start") + .to("github2:getCommitFile?repoOwner=acme&repoName=demo&oauthToken=not-used&encoding=UTF-8"); + } + }; + } + + @Test + void fileFromPathHeader() throws Exception { + GHContent content + = fileContent("base64", Base64.getMimeEncoder().encodeToString(TEXT.getBytes(StandardCharsets.UTF_8))); + when(repository.getFileContent(PATH, SHA)).thenReturn(content); + + String body = template.requestBodyAndHeaders("direct:start", "ignored", + Map.of(GitHub2Constants.GITHUB_COMMIT_SHA, SHA, GitHub2Constants.GITHUB_FILE_PATH, PATH), String.class); + + assertEquals(TEXT, body); + } + + @Test + void fileFromPathInBody() throws Exception { + GHContent content + = fileContent("base64", Base64.getMimeEncoder().encodeToString(TEXT.getBytes(StandardCharsets.UTF_8))); + when(repository.getFileContent(PATH, SHA)).thenReturn(content); + + String body = template.requestBodyAndHeader("direct:start", PATH, GitHub2Constants.GITHUB_COMMIT_SHA, SHA, + String.class); + + assertEquals(TEXT, body); + } + + @Test + void largeFileIsReadAsBlob() throws Exception { + // the contents API gives no content for a file of more than 1 MB + GHContent content = fileContent("none", ""); + when(content.getSha()).thenReturn("fedcba9876543210fedcba9876543210fedcba98"); + when(repository.getFileContent(PATH, SHA)).thenReturn(content); + GHBlob blob = mock(GHBlob.class); + when(blob.read()).thenReturn(new ByteArrayInputStream(TEXT.getBytes(StandardCharsets.UTF_8))); + when(repository.getBlob("fedcba9876543210fedcba9876543210fedcba98")).thenReturn(blob); + + String body = template.requestBodyAndHeader("direct:start", PATH, GitHub2Constants.GITHUB_COMMIT_SHA, SHA, + String.class); + + assertEquals(TEXT, body); + } + + @Test + void shaMustBeCommitId() throws Exception { + CamelExecutionException e = assertThrows(CamelExecutionException.class, + () -> template.requestBodyAndHeader("direct:start", PATH, GitHub2Constants.GITHUB_COMMIT_SHA, "main")); + + IllegalArgumentException cause = assertInstanceOf(IllegalArgumentException.class, e.getCause()); + assertTrue(cause.getMessage().contains("40 hexadecimal characters"), cause.getMessage()); + verify(repository, never()).getFileContent(anyString(), anyString()); + } + + @Test + void pathMustBeInTreeOfCommit() throws Exception { + CamelExecutionException e = assertThrows(CamelExecutionException.class, + () -> template.requestBodyAndHeader("direct:start", "docs/../readme.md", GitHub2Constants.GITHUB_COMMIT_SHA, + SHA)); + + IllegalArgumentException cause = assertInstanceOf(IllegalArgumentException.class, e.getCause()); + assertTrue(cause.getMessage().contains("path in the tree of the commit"), cause.getMessage()); + verify(repository, never()).getFileContent(anyString(), anyString()); + } + + @Test + void shaHeaderIsRequired() throws Exception { + CamelExecutionException e = assertThrows(CamelExecutionException.class, + () -> template.requestBody("direct:start", SHA)); + + IllegalArgumentException cause = assertInstanceOf(IllegalArgumentException.class, e.getCause()); + assertTrue(cause.getMessage().contains(GitHub2Constants.GITHUB_COMMIT_SHA), cause.getMessage()); + verify(repository, never()).getFileContent(anyString(), anyString()); + } + + private static GHContent fileContent(String encoding, String encodedContent) throws IOException { + GHContent content = mock(GHContent.class); + when(content.isFile()).thenReturn(true); + when(content.getType()).thenReturn("file"); + when(content.getEncoding()).thenReturn(encoding); + when(content.getEncodedContent()).thenReturn(encodedContent); + return content; + } + + private GitHub createGitHub() { + GitHub client = mock(GitHub.class); + try { + when(client.getRepository("acme/demo")).thenReturn(repository); + } catch (IOException e) { + throw new IllegalStateException(e); + } + return client; + } +} diff --git a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/GitHub2EndpointBuilderFactory.java b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/GitHub2EndpointBuilderFactory.java index 27b59b6735a8a..3fd618141e9e2 100644 --- a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/GitHub2EndpointBuilderFactory.java +++ b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/GitHub2EndpointBuilderFactory.java @@ -880,7 +880,8 @@ default GitHub2EndpointProducerBuilder repoOwner(String repoOwner) { return this; } /** - * To use the given encoding when getting a git commit file. + * The charset of the content of the file for the getCommitFile producer + * (the default charset of the JVM if not set). * * The option is a: java.lang.String type. * @@ -1246,17 +1247,31 @@ public String gitHubCommitCommitter() { return "CamelGitHubCommitCommitter"; } /** - * The commit sha. + * The commit sha. The getCommitFile producer reads the file at this + * commit (a commit id of 40 hexadecimal characters). * * The option is a: {@code String} type. * - * Group: consumer + * Group: common * * @return the name of the header {@code GitHubCommitSha}. */ public String gitHubCommitSha() { return "CamelGitHubCommitSha"; } + /** + * The path of the file in the repository for the getCommitFile + * producer. If not set, the body is the path. + * + * The option is a: {@code String} type. + * + * Group: producer + * + * @return the name of the header {@code GitHubFilePath}. + */ + public String gitHubFilePath() { + return "CamelGitHubFilePath"; + } /** * The commit URL. * From 1f77fc2df689d5a1b805f4619053c21315efc93e Mon Sep 17 00:00:00 2001 From: Claus Ibsen Date: Sat, 24 Oct 2026 08:17:07 +0200 Subject: [PATCH 2/2] CAMEL-25983: camel-github2 - getCommitFile uses the deprecated getEncodedContent on purpose Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: Claus Ibsen --- .../camel/component/github2/producer/GetCommitFileProducer.java | 1 + 1 file changed, 1 insertion(+) diff --git a/components/camel-github2/src/main/java/org/apache/camel/component/github2/producer/GetCommitFileProducer.java b/components/camel-github2/src/main/java/org/apache/camel/component/github2/producer/GetCommitFileProducer.java index cd4826b15aade..93c04b7807acb 100644 --- a/components/camel-github2/src/main/java/org/apache/camel/component/github2/producer/GetCommitFileProducer.java +++ b/components/camel-github2/src/main/java/org/apache/camel/component/github2/producer/GetCommitFileProducer.java @@ -91,6 +91,7 @@ public void process(Exchange exchange) throws Exception { exchange.getMessage().setBody(content); } + @SuppressWarnings("deprecation") private byte[] readContent(GHContent file) throws IOException { if ("base64".equals(file.getEncoding())) { // GitHub wraps the lines of the base64 content