Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Git support: first steps #109

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion source/dlangbot/app.d
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
74 changes: 74 additions & 0 deletions source/dlangbot/git.d
Original file line number Diff line number Diff line change
@@ -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));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spawnProcess > spawnShell in all situations, unless you need to use a shell built-in or run a user-supplied shell command

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;
}
1 change: 1 addition & 0 deletions source/dlangbot/github.d
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module dlangbot.github;

string githubURL = "https://github.com";
import dlangbot.bugzilla : bugzillaURL, Issue, IssueRef;
import dlangbot.warnings : printMessages, UserMessage;

Expand Down
4 changes: 3 additions & 1 deletion source/dlangbot/github_api.d
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module dlangbot.github_api;

string githubURL = "https://github.com";
string githubAPIURL = "https://api.github.com";
string githubAuth, hookSecret;

Expand Down Expand Up @@ -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); }
Expand All @@ -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
{
Expand Down
22 changes: 22 additions & 0 deletions test/git.d
Original file line number Diff line number Diff line change
@@ -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
}
72 changes: 0 additions & 72 deletions test/utils.d
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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)
Expand Down