Skip to content

Commit ac35495

Browse files
committed
Git support: first steps
1 parent 31db43b commit ac35495

File tree

6 files changed

+109
-74
lines changed

6 files changed

+109
-74
lines changed

source/dlangbot/app.d

+9-1
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,15 @@ void githubHook(HTTPServerRequest req, HTTPServerResponse res)
117117
action = "merged";
118118
goto case;
119119
case "opened", "reopened", "synchronize", "labeled", "edited":
120-
120+
if (action == "labeled")
121+
{
122+
if (json["label"]["name"].get!string == "bot-rebase")
123+
{
124+
import dlangbot.git : rebase;
125+
runTaskHelper(&rebase, &pullRequest);
126+
return res.writeBody("handled");
127+
}
128+
}
121129
runTaskHelper(&handlePR, action, &pullRequest);
122130
return res.writeBody("handled");
123131
default:

source/dlangbot/git.d

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
module dlangbot.git;
2+
3+
import std.conv, std.file, std.path, std.string, std.uuid;
4+
import std.format, std.stdio;
5+
6+
import dlangbot.github;
7+
import vibe.core.log;
8+
9+
string gitURL = "http://0.0.0.0:9006";
10+
11+
import std.process : Pid, ProcessPipes;
12+
13+
auto asyncWait(ProcessPipes p)
14+
{
15+
import core.sys.posix.fcntl;
16+
import core.time : seconds;
17+
import std.process : tryWait;
18+
import vibe.core.core : createFileDescriptorEvent, FileDescriptorEvent;
19+
20+
fcntl(p.stdout.fileno, F_SETFL, O_NONBLOCK);
21+
scope readEvt = createFileDescriptorEvent(p.stdout.fileno, FileDescriptorEvent.Trigger.read);
22+
while (readEvt.wait(5.seconds, FileDescriptorEvent.Trigger.read))
23+
{
24+
auto rc = tryWait(p.pid);
25+
if (rc.terminated)
26+
break;
27+
}
28+
}
29+
30+
auto asyncWait(Pid pid)
31+
{
32+
import core.time : msecs;
33+
import std.process : tryWait;
34+
import vibe.core.core : sleep;
35+
36+
for (auto rc = pid.tryWait; !rc.terminated; rc = pid.tryWait)
37+
5.msecs.sleep;
38+
}
39+
40+
void rebase(PullRequest* pr)
41+
{
42+
import std.process;
43+
auto uniqDir = tempDir.buildPath("dlang-bot-git", randomUUID.to!string.replace("-", ""));
44+
uniqDir.mkdirRecurse;
45+
scope(exit) uniqDir.rmdirRecurse;
46+
const git = "git -C %s ".format(uniqDir);
47+
48+
auto targetBranch = pr.base.ref_;
49+
auto remoteDir = pr.repoURL;
50+
51+
logInfo("[git/%s]: cloning branch %s...", pr.repoSlug, targetBranch);
52+
auto pid = spawnShell("git clone -b %s %s %s".format(targetBranch, remoteDir, uniqDir));
53+
pid.asyncWait;
54+
55+
logInfo("[git/%s]: fetching repo...", pr.repoSlug);
56+
pid = spawnShell(git ~ "fetch origin pull/%s/head:pr-%1$s".format(pr.number));
57+
pid.asyncWait;
58+
logInfo("[git/%s]: switching to PR branch...", pr.repoSlug);
59+
pid = spawnShell(git ~ "checkout pr-%s".format(pr.number));
60+
pid.asyncWait;
61+
logInfo("[git/%s]: rebasing...", pr.repoSlug);
62+
pid = spawnShell(git ~ "rebase " ~ targetBranch);
63+
pid.asyncWait;
64+
65+
auto headSlug = pr.head.repo.fullName;
66+
auto headRef = pr.head.ref_;
67+
auto sep = gitURL.startsWith("http") ? "/" : ":";
68+
logInfo("[git/%s]: pushing... to %s", pr.repoSlug, gitURL);
69+
70+
// TODO: use --force here
71+
auto cmd = "git push -vv %s%s%s HEAD:%s".format(gitURL, sep, headSlug, headRef);
72+
pid = spawnShell(cmd);
73+
pid.asyncWait;
74+
}

source/dlangbot/github.d

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
module dlangbot.github;
22

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

source/dlangbot/github_api.d

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
module dlangbot.github_api;
22

3+
string githubURL = "https://github.com";
34
string githubAPIURL = "https://api.github.com";
45
string githubAuth, hookSecret;
56

@@ -166,7 +167,7 @@ struct PullRequest
166167
alias repoSlug = baseRepoSlug;
167168
bool isOpen() const { return state == GHState.open; }
168169

169-
string htmlURL() const { return "https://github.com/%s/pull/%d".format(repoSlug, number); }
170+
string htmlURL() const { return "%s/%s/pull/%d".format(githubURL, repoSlug, number); }
170171
string commentsURL() const { return "%s/repos/%s/issues/%d/comments".format(githubAPIURL, repoSlug, number); }
171172
string reviewCommentsURL() const { return "%s/repos/%s/pulls/%d/comments".format(githubAPIURL, repoSlug, number); }
172173
string commitsURL() const { return "%s/repos/%s/pulls/%d/commits".format(githubAPIURL, repoSlug, number); }
@@ -176,6 +177,7 @@ struct PullRequest
176177
string mergeURL() const { return "%s/repos/%s/pulls/%d/merge".format(githubAPIURL, repoSlug, number); }
177178
string combinedStatusURL() const { return "%s/repos/%s/commits/%s/status".format(githubAPIURL, repoSlug, head.sha); }
178179
string membersURL() const { return "%s/orgs/%s/public_members".format(githubAPIURL, base.repo.owner.login); }
180+
string repoURL() const { return "%s/%s".format(githubURL, repoSlug); }
179181

180182
string pid() const
181183
{

test/git.d

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import utils;
2+
3+
// send rebase label
4+
unittest
5+
{
6+
setAPIExpectations();
7+
import std.stdio;
8+
9+
import std.array, std.conv, std.file, std.path, std.uuid;
10+
auto uniqDir = tempDir.buildPath("dlang-bot-git", randomUUID.to!string.replace("-", ""));
11+
uniqDir.mkdirRecurse;
12+
scope(exit) uniqDir.rmdirRecurse;
13+
14+
postGitHubHook("dlang_phobos_label_4921.json", "pull_request",
15+
(ref Json j, scope HTTPClientRequest req){
16+
j["head"]["repo"]["full_name"] = "/tmp/foobar";
17+
j["pull_request"]["state"] = "open";
18+
j["label"]["name"] = "bot-rebase";
19+
}.toDelegate);
20+
21+
// check result
22+
}

test/utils.d

-72
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@ void startFakeAPIServer()
6565
fakeSettings.port = getFreePort;
6666
fakeSettings.bindAddresses = ["0.0.0.0"];
6767
auto router = new URLRouter;
68-
router.any("*", &payloadServer);
6968

7069
listenHTTP(fakeSettings, router);
7170

@@ -77,77 +76,6 @@ void startFakeAPIServer()
7776
bugzillaURL = fakeAPIServerURL ~ "/bugzilla";
7877
}
7978

80-
// serves saved GitHub API payloads
81-
auto payloadServer(scope HTTPServerRequest req, scope HTTPServerResponse res)
82-
{
83-
import std.path, std.file;
84-
APIExpectation expectation = void;
85-
86-
// simple observer that checks whether a request is expected
87-
auto idx = apiExpectations.map!(x => x.url).countUntil(req.requestURL);
88-
if (idx >= 0)
89-
{
90-
expectation = apiExpectations[idx];
91-
if (apiExpectations.length > 1)
92-
apiExpectations = apiExpectations[0 .. idx] ~ apiExpectations[idx + 1 .. $];
93-
else
94-
apiExpectations.length = 0;
95-
}
96-
else
97-
{
98-
scope(failure) {
99-
writeln("Remaining expected URLs:", apiExpectations.map!(x => x.url));
100-
}
101-
assert(0, "Request for unexpected URL received: " ~ req.requestURL);
102-
}
103-
104-
res.statusCode = expectation.respStatusCode;
105-
// set failure status code exception to suppress false errors
106-
import dlangbot.utils : _expectedStatusCode;
107-
if (expectation.respStatusCode / 100 != 2)
108-
_expectedStatusCode = expectation.respStatusCode;
109-
110-
string filePath = buildPath(payloadDir, req.requestURL[1 .. $].replace("/", "_"));
111-
112-
if (expectation.reqHandler !is null)
113-
{
114-
scope(failure) {
115-
writefln("Method: %s", req.method);
116-
writefln("Json: %s", req.json);
117-
}
118-
expectation.reqHandler(req, res);
119-
if (res.headerWritten)
120-
return;
121-
if (!filePath.exists)
122-
return res.writeVoidBody;
123-
}
124-
125-
if (!filePath.exists)
126-
{
127-
assert(0, "Please create payload: " ~ filePath);
128-
}
129-
else
130-
{
131-
logInfo("reading payload: %s", filePath);
132-
auto payload = filePath.readText;
133-
if (req.requestURL.startsWith("/github", "/trello"))
134-
{
135-
auto payloadJson = payload.parseJsonString;
136-
replaceAPIReferences("https://api.github.com", githubAPIURL, payloadJson);
137-
replaceAPIReferences("https://api.trello.com", trelloAPIURL, payloadJson);
138-
139-
if (expectation.jsonHandler !is null)
140-
expectation.jsonHandler(payloadJson);
141-
142-
return res.writeJsonBody(payloadJson);
143-
}
144-
else
145-
{
146-
return res.writeBody(payload);
147-
}
148-
}
149-
}
150-
15179
void replaceAPIReferences(string official, string local, ref Json json)
15280
{
15381
void recursiveReplace(ref Json j)

0 commit comments

Comments
 (0)