From ac35495cc10242a01bc979c507b037bc0551725a Mon Sep 17 00:00:00 2001 From: Sebastian Wilzbach Date: Sun, 25 Jun 2017 22:52:08 +0200 Subject: [PATCH] Git support: first steps --- source/dlangbot/app.d | 10 ++++- source/dlangbot/git.d | 74 ++++++++++++++++++++++++++++++++++++ source/dlangbot/github.d | 1 + source/dlangbot/github_api.d | 4 +- test/git.d | 22 +++++++++++ test/utils.d | 72 ----------------------------------- 6 files changed, 109 insertions(+), 74 deletions(-) create mode 100644 source/dlangbot/git.d create mode 100644 test/git.d diff --git a/source/dlangbot/app.d b/source/dlangbot/app.d index 4bb8074..7572f51 100644 --- a/source/dlangbot/app.d +++ b/source/dlangbot/app.d @@ -117,7 +117,15 @@ void githubHook(HTTPServerRequest req, HTTPServerResponse res) action = "merged"; goto case; case "opened", "reopened", "synchronize", "labeled", "edited": - + if (action == "labeled") + { + if (json["label"]["name"].get!string == "bot-rebase") + { + import dlangbot.git : rebase; + runTaskHelper(&rebase, &pullRequest); + return res.writeBody("handled"); + } + } runTaskHelper(&handlePR, action, &pullRequest); return res.writeBody("handled"); default: diff --git a/source/dlangbot/git.d b/source/dlangbot/git.d new file mode 100644 index 0000000..47ec0f1 --- /dev/null +++ b/source/dlangbot/git.d @@ -0,0 +1,74 @@ +module dlangbot.git; + +import std.conv, std.file, std.path, std.string, std.uuid; +import std.format, std.stdio; + +import dlangbot.github; +import vibe.core.log; + +string gitURL = "http://0.0.0.0:9006"; + +import std.process : Pid, ProcessPipes; + +auto asyncWait(ProcessPipes p) +{ + import core.sys.posix.fcntl; + import core.time : seconds; + import std.process : tryWait; + import vibe.core.core : createFileDescriptorEvent, FileDescriptorEvent; + + fcntl(p.stdout.fileno, F_SETFL, O_NONBLOCK); + scope readEvt = createFileDescriptorEvent(p.stdout.fileno, FileDescriptorEvent.Trigger.read); + while (readEvt.wait(5.seconds, FileDescriptorEvent.Trigger.read)) + { + auto rc = tryWait(p.pid); + if (rc.terminated) + break; + } +} + +auto asyncWait(Pid pid) +{ + import core.time : msecs; + import std.process : tryWait; + import vibe.core.core : sleep; + + for (auto rc = pid.tryWait; !rc.terminated; rc = pid.tryWait) + 5.msecs.sleep; +} + +void rebase(PullRequest* pr) +{ + import std.process; + auto uniqDir = tempDir.buildPath("dlang-bot-git", randomUUID.to!string.replace("-", "")); + uniqDir.mkdirRecurse; + scope(exit) uniqDir.rmdirRecurse; + const git = "git -C %s ".format(uniqDir); + + auto targetBranch = pr.base.ref_; + auto remoteDir = pr.repoURL; + + logInfo("[git/%s]: cloning branch %s...", pr.repoSlug, targetBranch); + auto pid = spawnShell("git clone -b %s %s %s".format(targetBranch, remoteDir, uniqDir)); + pid.asyncWait; + + logInfo("[git/%s]: fetching repo...", pr.repoSlug); + pid = spawnShell(git ~ "fetch origin pull/%s/head:pr-%1$s".format(pr.number)); + pid.asyncWait; + logInfo("[git/%s]: switching to PR branch...", pr.repoSlug); + pid = spawnShell(git ~ "checkout pr-%s".format(pr.number)); + pid.asyncWait; + logInfo("[git/%s]: rebasing...", pr.repoSlug); + pid = spawnShell(git ~ "rebase " ~ targetBranch); + pid.asyncWait; + + auto headSlug = pr.head.repo.fullName; + auto headRef = pr.head.ref_; + auto sep = gitURL.startsWith("http") ? "/" : ":"; + logInfo("[git/%s]: pushing... to %s", pr.repoSlug, gitURL); + + // TODO: use --force here + auto cmd = "git push -vv %s%s%s HEAD:%s".format(gitURL, sep, headSlug, headRef); + pid = spawnShell(cmd); + pid.asyncWait; +} diff --git a/source/dlangbot/github.d b/source/dlangbot/github.d index d66953d..4967fe4 100644 --- a/source/dlangbot/github.d +++ b/source/dlangbot/github.d @@ -1,5 +1,6 @@ module dlangbot.github; +string githubURL = "https://github.com"; import dlangbot.bugzilla : bugzillaURL, Issue, IssueRef; import dlangbot.warnings : printMessages, UserMessage; diff --git a/source/dlangbot/github_api.d b/source/dlangbot/github_api.d index 37db950..64578ea 100644 --- a/source/dlangbot/github_api.d +++ b/source/dlangbot/github_api.d @@ -1,5 +1,6 @@ module dlangbot.github_api; +string githubURL = "https://github.com"; string githubAPIURL = "https://api.github.com"; string githubAuth, hookSecret; @@ -166,7 +167,7 @@ struct PullRequest alias repoSlug = baseRepoSlug; bool isOpen() const { return state == GHState.open; } - string htmlURL() const { return "https://github.com/%s/pull/%d".format(repoSlug, number); } + string htmlURL() const { return "%s/%s/pull/%d".format(githubURL, repoSlug, number); } string commentsURL() const { return "%s/repos/%s/issues/%d/comments".format(githubAPIURL, repoSlug, number); } string reviewCommentsURL() const { return "%s/repos/%s/pulls/%d/comments".format(githubAPIURL, repoSlug, number); } string commitsURL() const { return "%s/repos/%s/pulls/%d/commits".format(githubAPIURL, repoSlug, number); } @@ -176,6 +177,7 @@ struct PullRequest string mergeURL() const { return "%s/repos/%s/pulls/%d/merge".format(githubAPIURL, repoSlug, number); } string combinedStatusURL() const { return "%s/repos/%s/commits/%s/status".format(githubAPIURL, repoSlug, head.sha); } string membersURL() const { return "%s/orgs/%s/public_members".format(githubAPIURL, base.repo.owner.login); } + string repoURL() const { return "%s/%s".format(githubURL, repoSlug); } string pid() const { diff --git a/test/git.d b/test/git.d new file mode 100644 index 0000000..2661bb8 --- /dev/null +++ b/test/git.d @@ -0,0 +1,22 @@ +import utils; + +// send rebase label +unittest +{ + setAPIExpectations(); + import std.stdio; + + import std.array, std.conv, std.file, std.path, std.uuid; + auto uniqDir = tempDir.buildPath("dlang-bot-git", randomUUID.to!string.replace("-", "")); + uniqDir.mkdirRecurse; + scope(exit) uniqDir.rmdirRecurse; + + postGitHubHook("dlang_phobos_label_4921.json", "pull_request", + (ref Json j, scope HTTPClientRequest req){ + j["head"]["repo"]["full_name"] = "/tmp/foobar"; + j["pull_request"]["state"] = "open"; + j["label"]["name"] = "bot-rebase"; + }.toDelegate); + + // check result +} diff --git a/test/utils.d b/test/utils.d index a54cf62..061159d 100644 --- a/test/utils.d +++ b/test/utils.d @@ -65,7 +65,6 @@ void startFakeAPIServer() fakeSettings.port = getFreePort; fakeSettings.bindAddresses = ["0.0.0.0"]; auto router = new URLRouter; - router.any("*", &payloadServer); listenHTTP(fakeSettings, router); @@ -77,77 +76,6 @@ void startFakeAPIServer() bugzillaURL = fakeAPIServerURL ~ "/bugzilla"; } -// serves saved GitHub API payloads -auto payloadServer(scope HTTPServerRequest req, scope HTTPServerResponse res) -{ - import std.path, std.file; - APIExpectation expectation = void; - - // simple observer that checks whether a request is expected - auto idx = apiExpectations.map!(x => x.url).countUntil(req.requestURL); - if (idx >= 0) - { - expectation = apiExpectations[idx]; - if (apiExpectations.length > 1) - apiExpectations = apiExpectations[0 .. idx] ~ apiExpectations[idx + 1 .. $]; - else - apiExpectations.length = 0; - } - else - { - scope(failure) { - writeln("Remaining expected URLs:", apiExpectations.map!(x => x.url)); - } - assert(0, "Request for unexpected URL received: " ~ req.requestURL); - } - - res.statusCode = expectation.respStatusCode; - // set failure status code exception to suppress false errors - import dlangbot.utils : _expectedStatusCode; - if (expectation.respStatusCode / 100 != 2) - _expectedStatusCode = expectation.respStatusCode; - - string filePath = buildPath(payloadDir, req.requestURL[1 .. $].replace("/", "_")); - - if (expectation.reqHandler !is null) - { - scope(failure) { - writefln("Method: %s", req.method); - writefln("Json: %s", req.json); - } - expectation.reqHandler(req, res); - if (res.headerWritten) - return; - if (!filePath.exists) - return res.writeVoidBody; - } - - if (!filePath.exists) - { - assert(0, "Please create payload: " ~ filePath); - } - else - { - logInfo("reading payload: %s", filePath); - auto payload = filePath.readText; - if (req.requestURL.startsWith("/github", "/trello")) - { - auto payloadJson = payload.parseJsonString; - replaceAPIReferences("https://api.github.com", githubAPIURL, payloadJson); - replaceAPIReferences("https://api.trello.com", trelloAPIURL, payloadJson); - - if (expectation.jsonHandler !is null) - expectation.jsonHandler(payloadJson); - - return res.writeJsonBody(payloadJson); - } - else - { - return res.writeBody(payload); - } - } -} - void replaceAPIReferences(string official, string local, ref Json json) { void recursiveReplace(ref Json j)