From 300fa91a8ae065b7f6f6370882b10929bbde6309 Mon Sep 17 00:00:00 2001 From: Ratchanan Srirattanamet Date: Tue, 20 Feb 2024 20:24:08 +0700 Subject: [PATCH] fix: --no-squash on single-commit GitLab MR (#93) Due to off-by-one error in GitLabMapper, --no-squash was not effective on an MR containing single commit. Fix by using the same condition as GitHubMapper. --- dist/cli/index.js | 212 +++++++++++------- dist/gha/index.js | 212 +++++++++++------- src/service/git/gitlab/gitlab-mapper.ts | 4 +- test/service/runner/cli-gitlab-runner.test.ts | 44 ++++ test/service/runner/gha-gitlab-runner.test.ts | 42 ++++ test/support/mock/git-client-mock-support.ts | 4 +- test/support/mock/gitlab-data.ts | 23 ++ 7 files changed, 376 insertions(+), 165 deletions(-) diff --git a/dist/cli/index.js b/dist/cli/index.js index 72d9723..5a84866 100755 --- a/dist/cli/index.js +++ b/dist/cli/index.js @@ -1165,8 +1165,8 @@ class GitLabMapper { sourceRepo: await this.mapSourceRepo(mr), targetRepo: await this.mapTargetRepo(mr), // if commits list is provided use that as source - nCommits: (commits && commits.length > 1) ? commits.length : 1, - commits: (commits && commits.length > 1) ? commits : this.getSha(mr) + nCommits: (commits && commits.length > 0) ? commits.length : 1, + commits: (commits && commits.length > 0) ? commits : this.getSha(mr) }; } getSha(mr) { @@ -5847,6 +5847,29 @@ var Writable = (__nccwpck_require__(2781).Writable); var assert = __nccwpck_require__(9491); var debug = __nccwpck_require__(1133); +// Whether to use the native URL object or the legacy url module +var useNativeURL = false; +try { + assert(new URL()); +} +catch (error) { + useNativeURL = error.code === "ERR_INVALID_URL"; +} + +// URL fields to preserve in copy operations +var preservedUrlFields = [ + "auth", + "host", + "hostname", + "href", + "path", + "pathname", + "port", + "protocol", + "query", + "search", +]; + // Create handlers that pass events from native requests var events = ["abort", "aborted", "connect", "error", "socket", "timeout"]; var eventHandlers = Object.create(null); @@ -5856,19 +5879,20 @@ events.forEach(function (event) { }; }); +// Error types with codes var InvalidUrlError = createErrorType( "ERR_INVALID_URL", "Invalid URL", TypeError ); -// Error types with codes var RedirectionError = createErrorType( "ERR_FR_REDIRECTION_FAILURE", "Redirected request failed" ); var TooManyRedirectsError = createErrorType( "ERR_FR_TOO_MANY_REDIRECTS", - "Maximum number of redirects exceeded" + "Maximum number of redirects exceeded", + RedirectionError ); var MaxBodyLengthExceededError = createErrorType( "ERR_FR_MAX_BODY_LENGTH_EXCEEDED", @@ -5879,6 +5903,9 @@ var WriteAfterEndError = createErrorType( "write after end" ); +// istanbul ignore next +var destroy = Writable.prototype.destroy || noop; + // An HTTP(S) request that can be redirected function RedirectableRequest(options, responseCallback) { // Initialize the request @@ -5900,7 +5927,13 @@ function RedirectableRequest(options, responseCallback) { // React to responses of native requests var self = this; this._onNativeResponse = function (response) { - self._processResponse(response); + try { + self._processResponse(response); + } + catch (cause) { + self.emit("error", cause instanceof RedirectionError ? + cause : new RedirectionError({ cause: cause })); + } }; // Perform the first request @@ -5909,10 +5942,17 @@ function RedirectableRequest(options, responseCallback) { RedirectableRequest.prototype = Object.create(Writable.prototype); RedirectableRequest.prototype.abort = function () { - abortRequest(this._currentRequest); + destroyRequest(this._currentRequest); + this._currentRequest.abort(); this.emit("abort"); }; +RedirectableRequest.prototype.destroy = function (error) { + destroyRequest(this._currentRequest, error); + destroy.call(this, error); + return this; +}; + // Writes buffered data to the current native request RedirectableRequest.prototype.write = function (data, encoding, callback) { // Writing is not allowed if end has been called @@ -6025,6 +6065,7 @@ RedirectableRequest.prototype.setTimeout = function (msecs, callback) { self.removeListener("abort", clearTimer); self.removeListener("error", clearTimer); self.removeListener("response", clearTimer); + self.removeListener("close", clearTimer); if (callback) { self.removeListener("timeout", callback); } @@ -6051,6 +6092,7 @@ RedirectableRequest.prototype.setTimeout = function (msecs, callback) { this.on("abort", clearTimer); this.on("error", clearTimer); this.on("response", clearTimer); + this.on("close", clearTimer); return this; }; @@ -6109,8 +6151,7 @@ RedirectableRequest.prototype._performRequest = function () { var protocol = this._options.protocol; var nativeProtocol = this._options.nativeProtocols[protocol]; if (!nativeProtocol) { - this.emit("error", new TypeError("Unsupported protocol " + protocol)); - return; + throw new TypeError("Unsupported protocol " + protocol); } // If specified, use the agent corresponding to the protocol @@ -6202,15 +6243,14 @@ RedirectableRequest.prototype._processResponse = function (response) { } // The response is a redirect, so abort the current request - abortRequest(this._currentRequest); + destroyRequest(this._currentRequest); // Discard the remainder of the response to avoid waiting for data response.destroy(); // RFC7231§6.4: A client SHOULD detect and intervene // in cyclical redirections (i.e., "infinite" redirection loops). if (++this._redirectCount > this._options.maxRedirects) { - this.emit("error", new TooManyRedirectsError()); - return; + throw new TooManyRedirectsError(); } // Store the request headers if applicable @@ -6244,33 +6284,23 @@ RedirectableRequest.prototype._processResponse = function (response) { var currentHostHeader = removeMatchingHeaders(/^host$/i, this._options.headers); // If the redirect is relative, carry over the host of the last request - var currentUrlParts = url.parse(this._currentUrl); + var currentUrlParts = parseUrl(this._currentUrl); var currentHost = currentHostHeader || currentUrlParts.host; var currentUrl = /^\w+:/.test(location) ? this._currentUrl : url.format(Object.assign(currentUrlParts, { host: currentHost })); - // Determine the URL of the redirection - var redirectUrl; - try { - redirectUrl = url.resolve(currentUrl, location); - } - catch (cause) { - this.emit("error", new RedirectionError({ cause: cause })); - return; - } - // Create the redirected request - debug("redirecting to", redirectUrl); + var redirectUrl = resolveUrl(location, currentUrl); + debug("redirecting to", redirectUrl.href); this._isRedirect = true; - var redirectUrlParts = url.parse(redirectUrl); - Object.assign(this._options, redirectUrlParts); + spreadUrlObject(redirectUrl, this._options); // Drop confidential headers when redirecting to a less secure protocol // or to a different domain that is not a superdomain - if (redirectUrlParts.protocol !== currentUrlParts.protocol && - redirectUrlParts.protocol !== "https:" || - redirectUrlParts.host !== currentHost && - !isSubdomain(redirectUrlParts.host, currentHost)) { + if (redirectUrl.protocol !== currentUrlParts.protocol && + redirectUrl.protocol !== "https:" || + redirectUrl.host !== currentHost && + !isSubdomain(redirectUrl.host, currentHost)) { removeMatchingHeaders(/^(?:authorization|cookie)$/i, this._options.headers); } @@ -6285,23 +6315,12 @@ RedirectableRequest.prototype._processResponse = function (response) { method: method, headers: requestHeaders, }; - try { - beforeRedirect(this._options, responseDetails, requestDetails); - } - catch (err) { - this.emit("error", err); - return; - } + beforeRedirect(this._options, responseDetails, requestDetails); this._sanitizeOptions(this._options); } // Perform the redirected request - try { - this._performRequest(); - } - catch (cause) { - this.emit("error", new RedirectionError({ cause: cause })); - } + this._performRequest(); }; // Wraps the key/value object of protocols with redirect functionality @@ -6321,27 +6340,16 @@ function wrap(protocols) { // Executes a request, following redirects function request(input, options, callback) { - // Parse parameters - if (isString(input)) { - var parsed; - try { - parsed = urlToOptions(new URL(input)); - } - catch (err) { - /* istanbul ignore next */ - parsed = url.parse(input); - } - if (!isString(parsed.protocol)) { - throw new InvalidUrlError({ input }); - } - input = parsed; + // Parse parameters, ensuring that input is an object + if (isURL(input)) { + input = spreadUrlObject(input); } - else if (URL && (input instanceof URL)) { - input = urlToOptions(input); + else if (isString(input)) { + input = spreadUrlObject(parseUrl(input)); } else { callback = options; - options = input; + options = validateUrl(input); input = { protocol: protocol }; } if (isFunction(options)) { @@ -6380,27 +6388,57 @@ function wrap(protocols) { return exports; } -/* istanbul ignore next */ function noop() { /* empty */ } -// from https://github.com/nodejs/node/blob/master/lib/internal/url.js -function urlToOptions(urlObject) { - var options = { - protocol: urlObject.protocol, - hostname: urlObject.hostname.startsWith("[") ? - /* istanbul ignore next */ - urlObject.hostname.slice(1, -1) : - urlObject.hostname, - hash: urlObject.hash, - search: urlObject.search, - pathname: urlObject.pathname, - path: urlObject.pathname + urlObject.search, - href: urlObject.href, - }; - if (urlObject.port !== "") { - options.port = Number(urlObject.port); +function parseUrl(input) { + var parsed; + /* istanbul ignore else */ + if (useNativeURL) { + parsed = new URL(input); + } + else { + // Ensure the URL is valid and absolute + parsed = validateUrl(url.parse(input)); + if (!isString(parsed.protocol)) { + throw new InvalidUrlError({ input }); + } + } + return parsed; +} + +function resolveUrl(relative, base) { + /* istanbul ignore next */ + return useNativeURL ? new URL(relative, base) : parseUrl(url.resolve(base, relative)); +} + +function validateUrl(input) { + if (/^\[/.test(input.hostname) && !/^\[[:0-9a-f]+\]$/i.test(input.hostname)) { + throw new InvalidUrlError({ input: input.href || input }); + } + if (/^\[/.test(input.host) && !/^\[[:0-9a-f]+\](:\d+)?$/i.test(input.host)) { + throw new InvalidUrlError({ input: input.href || input }); } - return options; + return input; +} + +function spreadUrlObject(urlObject, target) { + var spread = target || {}; + for (var key of preservedUrlFields) { + spread[key] = urlObject[key]; + } + + // Fix IPv6 hostname + if (spread.hostname.startsWith("[")) { + spread.hostname = spread.hostname.slice(1, -1); + } + // Ensure port is a number + if (spread.port !== "") { + spread.port = Number(spread.port); + } + // Concatenate path + spread.path = spread.search ? spread.pathname + spread.search : spread.pathname; + + return spread; } function removeMatchingHeaders(regex, headers) { @@ -6426,17 +6464,25 @@ function createErrorType(code, message, baseClass) { // Attach constructor and set default properties CustomError.prototype = new (baseClass || Error)(); - CustomError.prototype.constructor = CustomError; - CustomError.prototype.name = "Error [" + code + "]"; + Object.defineProperties(CustomError.prototype, { + constructor: { + value: CustomError, + enumerable: false, + }, + name: { + value: "Error [" + code + "]", + enumerable: false, + }, + }); return CustomError; } -function abortRequest(request) { +function destroyRequest(request, error) { for (var event of events) { request.removeListener(event, eventHandlers[event]); } request.on("error", noop); - request.abort(); + request.destroy(error); } function isSubdomain(subdomain, domain) { @@ -6457,6 +6503,10 @@ function isBuffer(value) { return typeof value === "object" && ("length" in value); } +function isURL(value) { + return URL && value instanceof URL; +} + // Exports module.exports = wrap({ http: http, https: https }); module.exports.wrap = wrap; diff --git a/dist/gha/index.js b/dist/gha/index.js index 344c82f..9f2c8e4 100755 --- a/dist/gha/index.js +++ b/dist/gha/index.js @@ -1135,8 +1135,8 @@ class GitLabMapper { sourceRepo: await this.mapSourceRepo(mr), targetRepo: await this.mapTargetRepo(mr), // if commits list is provided use that as source - nCommits: (commits && commits.length > 1) ? commits.length : 1, - commits: (commits && commits.length > 1) ? commits : this.getSha(mr) + nCommits: (commits && commits.length > 0) ? commits.length : 1, + commits: (commits && commits.length > 0) ? commits : this.getSha(mr) }; } getSha(mr) { @@ -7578,6 +7578,29 @@ var Writable = (__nccwpck_require__(2781).Writable); var assert = __nccwpck_require__(9491); var debug = __nccwpck_require__(1133); +// Whether to use the native URL object or the legacy url module +var useNativeURL = false; +try { + assert(new URL()); +} +catch (error) { + useNativeURL = error.code === "ERR_INVALID_URL"; +} + +// URL fields to preserve in copy operations +var preservedUrlFields = [ + "auth", + "host", + "hostname", + "href", + "path", + "pathname", + "port", + "protocol", + "query", + "search", +]; + // Create handlers that pass events from native requests var events = ["abort", "aborted", "connect", "error", "socket", "timeout"]; var eventHandlers = Object.create(null); @@ -7587,19 +7610,20 @@ events.forEach(function (event) { }; }); +// Error types with codes var InvalidUrlError = createErrorType( "ERR_INVALID_URL", "Invalid URL", TypeError ); -// Error types with codes var RedirectionError = createErrorType( "ERR_FR_REDIRECTION_FAILURE", "Redirected request failed" ); var TooManyRedirectsError = createErrorType( "ERR_FR_TOO_MANY_REDIRECTS", - "Maximum number of redirects exceeded" + "Maximum number of redirects exceeded", + RedirectionError ); var MaxBodyLengthExceededError = createErrorType( "ERR_FR_MAX_BODY_LENGTH_EXCEEDED", @@ -7610,6 +7634,9 @@ var WriteAfterEndError = createErrorType( "write after end" ); +// istanbul ignore next +var destroy = Writable.prototype.destroy || noop; + // An HTTP(S) request that can be redirected function RedirectableRequest(options, responseCallback) { // Initialize the request @@ -7631,7 +7658,13 @@ function RedirectableRequest(options, responseCallback) { // React to responses of native requests var self = this; this._onNativeResponse = function (response) { - self._processResponse(response); + try { + self._processResponse(response); + } + catch (cause) { + self.emit("error", cause instanceof RedirectionError ? + cause : new RedirectionError({ cause: cause })); + } }; // Perform the first request @@ -7640,10 +7673,17 @@ function RedirectableRequest(options, responseCallback) { RedirectableRequest.prototype = Object.create(Writable.prototype); RedirectableRequest.prototype.abort = function () { - abortRequest(this._currentRequest); + destroyRequest(this._currentRequest); + this._currentRequest.abort(); this.emit("abort"); }; +RedirectableRequest.prototype.destroy = function (error) { + destroyRequest(this._currentRequest, error); + destroy.call(this, error); + return this; +}; + // Writes buffered data to the current native request RedirectableRequest.prototype.write = function (data, encoding, callback) { // Writing is not allowed if end has been called @@ -7756,6 +7796,7 @@ RedirectableRequest.prototype.setTimeout = function (msecs, callback) { self.removeListener("abort", clearTimer); self.removeListener("error", clearTimer); self.removeListener("response", clearTimer); + self.removeListener("close", clearTimer); if (callback) { self.removeListener("timeout", callback); } @@ -7782,6 +7823,7 @@ RedirectableRequest.prototype.setTimeout = function (msecs, callback) { this.on("abort", clearTimer); this.on("error", clearTimer); this.on("response", clearTimer); + this.on("close", clearTimer); return this; }; @@ -7840,8 +7882,7 @@ RedirectableRequest.prototype._performRequest = function () { var protocol = this._options.protocol; var nativeProtocol = this._options.nativeProtocols[protocol]; if (!nativeProtocol) { - this.emit("error", new TypeError("Unsupported protocol " + protocol)); - return; + throw new TypeError("Unsupported protocol " + protocol); } // If specified, use the agent corresponding to the protocol @@ -7933,15 +7974,14 @@ RedirectableRequest.prototype._processResponse = function (response) { } // The response is a redirect, so abort the current request - abortRequest(this._currentRequest); + destroyRequest(this._currentRequest); // Discard the remainder of the response to avoid waiting for data response.destroy(); // RFC7231§6.4: A client SHOULD detect and intervene // in cyclical redirections (i.e., "infinite" redirection loops). if (++this._redirectCount > this._options.maxRedirects) { - this.emit("error", new TooManyRedirectsError()); - return; + throw new TooManyRedirectsError(); } // Store the request headers if applicable @@ -7975,33 +8015,23 @@ RedirectableRequest.prototype._processResponse = function (response) { var currentHostHeader = removeMatchingHeaders(/^host$/i, this._options.headers); // If the redirect is relative, carry over the host of the last request - var currentUrlParts = url.parse(this._currentUrl); + var currentUrlParts = parseUrl(this._currentUrl); var currentHost = currentHostHeader || currentUrlParts.host; var currentUrl = /^\w+:/.test(location) ? this._currentUrl : url.format(Object.assign(currentUrlParts, { host: currentHost })); - // Determine the URL of the redirection - var redirectUrl; - try { - redirectUrl = url.resolve(currentUrl, location); - } - catch (cause) { - this.emit("error", new RedirectionError({ cause: cause })); - return; - } - // Create the redirected request - debug("redirecting to", redirectUrl); + var redirectUrl = resolveUrl(location, currentUrl); + debug("redirecting to", redirectUrl.href); this._isRedirect = true; - var redirectUrlParts = url.parse(redirectUrl); - Object.assign(this._options, redirectUrlParts); + spreadUrlObject(redirectUrl, this._options); // Drop confidential headers when redirecting to a less secure protocol // or to a different domain that is not a superdomain - if (redirectUrlParts.protocol !== currentUrlParts.protocol && - redirectUrlParts.protocol !== "https:" || - redirectUrlParts.host !== currentHost && - !isSubdomain(redirectUrlParts.host, currentHost)) { + if (redirectUrl.protocol !== currentUrlParts.protocol && + redirectUrl.protocol !== "https:" || + redirectUrl.host !== currentHost && + !isSubdomain(redirectUrl.host, currentHost)) { removeMatchingHeaders(/^(?:authorization|cookie)$/i, this._options.headers); } @@ -8016,23 +8046,12 @@ RedirectableRequest.prototype._processResponse = function (response) { method: method, headers: requestHeaders, }; - try { - beforeRedirect(this._options, responseDetails, requestDetails); - } - catch (err) { - this.emit("error", err); - return; - } + beforeRedirect(this._options, responseDetails, requestDetails); this._sanitizeOptions(this._options); } // Perform the redirected request - try { - this._performRequest(); - } - catch (cause) { - this.emit("error", new RedirectionError({ cause: cause })); - } + this._performRequest(); }; // Wraps the key/value object of protocols with redirect functionality @@ -8052,27 +8071,16 @@ function wrap(protocols) { // Executes a request, following redirects function request(input, options, callback) { - // Parse parameters - if (isString(input)) { - var parsed; - try { - parsed = urlToOptions(new URL(input)); - } - catch (err) { - /* istanbul ignore next */ - parsed = url.parse(input); - } - if (!isString(parsed.protocol)) { - throw new InvalidUrlError({ input }); - } - input = parsed; + // Parse parameters, ensuring that input is an object + if (isURL(input)) { + input = spreadUrlObject(input); } - else if (URL && (input instanceof URL)) { - input = urlToOptions(input); + else if (isString(input)) { + input = spreadUrlObject(parseUrl(input)); } else { callback = options; - options = input; + options = validateUrl(input); input = { protocol: protocol }; } if (isFunction(options)) { @@ -8111,27 +8119,57 @@ function wrap(protocols) { return exports; } -/* istanbul ignore next */ function noop() { /* empty */ } -// from https://github.com/nodejs/node/blob/master/lib/internal/url.js -function urlToOptions(urlObject) { - var options = { - protocol: urlObject.protocol, - hostname: urlObject.hostname.startsWith("[") ? - /* istanbul ignore next */ - urlObject.hostname.slice(1, -1) : - urlObject.hostname, - hash: urlObject.hash, - search: urlObject.search, - pathname: urlObject.pathname, - path: urlObject.pathname + urlObject.search, - href: urlObject.href, - }; - if (urlObject.port !== "") { - options.port = Number(urlObject.port); +function parseUrl(input) { + var parsed; + /* istanbul ignore else */ + if (useNativeURL) { + parsed = new URL(input); + } + else { + // Ensure the URL is valid and absolute + parsed = validateUrl(url.parse(input)); + if (!isString(parsed.protocol)) { + throw new InvalidUrlError({ input }); + } + } + return parsed; +} + +function resolveUrl(relative, base) { + /* istanbul ignore next */ + return useNativeURL ? new URL(relative, base) : parseUrl(url.resolve(base, relative)); +} + +function validateUrl(input) { + if (/^\[/.test(input.hostname) && !/^\[[:0-9a-f]+\]$/i.test(input.hostname)) { + throw new InvalidUrlError({ input: input.href || input }); + } + if (/^\[/.test(input.host) && !/^\[[:0-9a-f]+\](:\d+)?$/i.test(input.host)) { + throw new InvalidUrlError({ input: input.href || input }); } - return options; + return input; +} + +function spreadUrlObject(urlObject, target) { + var spread = target || {}; + for (var key of preservedUrlFields) { + spread[key] = urlObject[key]; + } + + // Fix IPv6 hostname + if (spread.hostname.startsWith("[")) { + spread.hostname = spread.hostname.slice(1, -1); + } + // Ensure port is a number + if (spread.port !== "") { + spread.port = Number(spread.port); + } + // Concatenate path + spread.path = spread.search ? spread.pathname + spread.search : spread.pathname; + + return spread; } function removeMatchingHeaders(regex, headers) { @@ -8157,17 +8195,25 @@ function createErrorType(code, message, baseClass) { // Attach constructor and set default properties CustomError.prototype = new (baseClass || Error)(); - CustomError.prototype.constructor = CustomError; - CustomError.prototype.name = "Error [" + code + "]"; + Object.defineProperties(CustomError.prototype, { + constructor: { + value: CustomError, + enumerable: false, + }, + name: { + value: "Error [" + code + "]", + enumerable: false, + }, + }); return CustomError; } -function abortRequest(request) { +function destroyRequest(request, error) { for (var event of events) { request.removeListener(event, eventHandlers[event]); } request.on("error", noop); - request.abort(); + request.destroy(error); } function isSubdomain(subdomain, domain) { @@ -8188,6 +8234,10 @@ function isBuffer(value) { return typeof value === "object" && ("length" in value); } +function isURL(value) { + return URL && value instanceof URL; +} + // Exports module.exports = wrap({ http: http, https: https }); module.exports.wrap = wrap; diff --git a/src/service/git/gitlab/gitlab-mapper.ts b/src/service/git/gitlab/gitlab-mapper.ts index 5765ba6..9efe2a0 100644 --- a/src/service/git/gitlab/gitlab-mapper.ts +++ b/src/service/git/gitlab/gitlab-mapper.ts @@ -41,8 +41,8 @@ export default class GitLabMapper implements GitResponseMapper 1) ? commits.length : 1, - commits: (commits && commits.length > 1) ? commits : this.getSha(mr) + nCommits: (commits && commits.length > 0) ? commits.length : 1, + commits: (commits && commits.length > 0) ? commits : this.getSha(mr) }; } diff --git a/test/service/runner/cli-gitlab-runner.test.ts b/test/service/runner/cli-gitlab-runner.test.ts index ab6f72b..92dc4d7 100644 --- a/test/service/runner/cli-gitlab-runner.test.ts +++ b/test/service/runner/cli-gitlab-runner.test.ts @@ -504,6 +504,50 @@ describe("cli runner", () => { ); }); + test("single commit without squash", async () => { + addProcessArgs([ + "-tb", + "target", + "-pr", + "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1", + "--no-squash", + ]); + + await runner.execute(); + + const cwd = process.cwd() + "/bp"; + + expect(GitClientFactory.getOrCreate).toBeCalledTimes(1); + expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITLAB, undefined, "https://my.gitlab.host.com/api/v4"); + + expect(GitCLIService.prototype.clone).toBeCalledTimes(1); + expect(GitCLIService.prototype.clone).toBeCalledWith("https://my.gitlab.host.com/superuser/backporting-example.git", cwd, "target"); + + expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1); + expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-e4dd336"); + + expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1); + expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "e4dd336a4a20f394df6665994df382fb1d193a11", undefined, undefined); + + expect(GitCLIService.prototype.push).toBeCalledTimes(1); + expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-e4dd336"); + + expect(GitLabClient.prototype.createPullRequest).toBeCalledTimes(1); + expect(GitLabClient.prototype.createPullRequest).toBeCalledWith({ + owner: "superuser", + repo: "backporting-example", + head: "bp-target-e4dd336", + base: "target", + title: "[target] Update test.txt", + body: expect.stringContaining("**Backport:** https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1"), + reviewers: ["superuser"], + assignees: [], + labels: [], + comments: [], + } + ); + }); + test("multiple commits without squash", async () => { addProcessArgs([ "-tb", diff --git a/test/service/runner/gha-gitlab-runner.test.ts b/test/service/runner/gha-gitlab-runner.test.ts index 8158818..748ca15 100644 --- a/test/service/runner/gha-gitlab-runner.test.ts +++ b/test/service/runner/gha-gitlab-runner.test.ts @@ -427,6 +427,48 @@ describe("gha runner", () => { ); }); + test("single commit without squash", async () => { + spyGetInput({ + "target-branch": "target", + "pull-request": "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1", + "no-squash": "true", + }); + + await runner.execute(); + + const cwd = process.cwd() + "/bp"; + + expect(GitClientFactory.getOrCreate).toBeCalledTimes(1); + expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITLAB, undefined, "https://my.gitlab.host.com/api/v4"); + + expect(GitCLIService.prototype.clone).toBeCalledTimes(1); + expect(GitCLIService.prototype.clone).toBeCalledWith("https://my.gitlab.host.com/superuser/backporting-example.git", cwd, "target"); + + expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1); + expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-e4dd336"); + + expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1); + expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "e4dd336a4a20f394df6665994df382fb1d193a11", undefined, undefined); + + expect(GitCLIService.prototype.push).toBeCalledTimes(1); + expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-e4dd336"); + + expect(GitLabClient.prototype.createPullRequest).toBeCalledTimes(1); + expect(GitLabClient.prototype.createPullRequest).toBeCalledWith({ + owner: "superuser", + repo: "backporting-example", + head: "bp-target-e4dd336", + base: "target", + title: "[target] Update test.txt", + body: expect.stringContaining("**Backport:** https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1"), + reviewers: ["superuser"], + assignees: [], + labels: [], + comments: [], + } + ); + }); + test("multiple commits without squash", async () => { spyGetInput({ "target-branch": "target", diff --git a/test/support/mock/git-client-mock-support.ts b/test/support/mock/git-client-mock-support.ts index b1c2886..7dceabc 100644 --- a/test/support/mock/git-client-mock-support.ts +++ b/test/support/mock/git-client-mock-support.ts @@ -1,7 +1,7 @@ import LoggerServiceFactory from "@bp/service/logger/logger-service-factory"; import { Moctokit } from "@kie/mock-github"; import { TARGET_OWNER, REPO, MERGED_PR_FIXTURE, OPEN_PR_FIXTURE, NOT_MERGED_PR_FIXTURE, NOT_FOUND_PR_NUMBER, MULT_COMMITS_PR_FIXTURE, MULT_COMMITS_PR_COMMITS, NEW_PR_URL, NEW_PR_NUMBER } from "./github-data"; -import { CLOSED_NOT_MERGED_MR, MERGED_SQUASHED_MR, NESTED_NAMESPACE_MR, OPEN_MR, OPEN_PR_COMMITS, PROJECT_EXAMPLE, NESTED_PROJECT_EXAMPLE, SUPERUSER} from "./gitlab-data"; +import { CLOSED_NOT_MERGED_MR, MERGED_SQUASHED_MR, NESTED_NAMESPACE_MR, OPEN_MR, OPEN_PR_COMMITS, PROJECT_EXAMPLE, NESTED_PROJECT_EXAMPLE, SUPERUSER, MERGED_SQUASHED_MR_COMMITS } from "./gitlab-data"; // high number, for each test we are not expecting // to send more than 3 reqs per api endpoint @@ -30,6 +30,8 @@ export const getAxiosMocked = (url: string) => { data = NESTED_PROJECT_EXAMPLE; } else if (url.endsWith("users?username=superuser")) { data = [SUPERUSER]; + } else if (url.endsWith("merge_requests/1/commits")) { + data = MERGED_SQUASHED_MR_COMMITS; } else if (url.endsWith("merge_requests/2/commits")) { data = OPEN_PR_COMMITS; } diff --git a/test/support/mock/gitlab-data.ts b/test/support/mock/gitlab-data.ts index a7455fc..3f32b81 100644 --- a/test/support/mock/gitlab-data.ts +++ b/test/support/mock/gitlab-data.ts @@ -689,6 +689,29 @@ export const CLOSED_NOT_MERGED_MR = { } }; +export const MERGED_SQUASHED_MR_COMMITS = [ + { + "id":"e4dd336a4a20f394df6665994df382fb1d193a11", + "short_id":"e4dd336a", + "created_at":"2023-06-29T09:59:10.000Z", + "parent_ids":[ + + ], + "title":"Add new file", + "message":"Add new file", + "author_name":"Super User", + "author_email":"superuser@email.com", + "authored_date":"2023-06-29T09:59:10.000Z", + "committer_name":"Super User", + "committer_email":"superuser@email.com", + "committed_date":"2023-06-29T09:59:10.000Z", + "trailers":{ + + }, + "web_url":"https://gitlab.com/superuser/backporting-example/-/commit/e4dd336a4a20f394df6665994df382fb1d193a11" + }, +]; + export const OPEN_PR_COMMITS = [ { "id":"974519f65c9e0ed65277cd71026657a09fca05e7",