提交 3dcd2c06 编写于 作者: B Brett V. Forsgren

add VisualStudioOnline support to merge bot

上级 e825ae47
......@@ -26,7 +26,7 @@ public override async Task<bool> ShouldMakePullRequestAsync(string title)
return (await GetExistingMergePrsAsync(title)).Count == 0;
}
public override async Task CreatePullRequestAsync(string title, string destinationOwner, string pullRequestBranch, string sourceBranch, string destinationBranch)
public override async Task CreatePullRequestAsync(string title, string destinationOwner, string pullRequestBranch, string prBranchSourceRemote, string sourceBranch, string destinationBranch)
{
var remoteName = $"{UserName}-{RepositoryName}";
var prMessage = $@"
......@@ -37,10 +37,10 @@ public override async Task CreatePullRequestAsync(string title, string destinati
``` bash
git remote add {remoteName} ""https://github.com/{UserName}/{RepositoryName}.git""
git fetch {remoteName}
git fetch upstream
git fetch {prBranchSourceRemote}
git checkout {pullRequestBranch}
git reset --hard upstream/{destinationBranch}
git merge upstream/{sourceBranch}
git reset --hard {prBranchSourceRemote}/{destinationBranch}
git merge {prBranchSourceRemote}/{sourceBranch}
# Fix merge conflicts
git commit
git push {remoteName} {pullRequestBranch} --force
......
......@@ -39,6 +39,10 @@
<HintPath>..\..\..\..\..\packages\LibGit2Sharp.0.22.0\lib\net40\LibGit2Sharp.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Newtonsoft.Json, Version=9.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\..\..\..\..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Octokit, Version=0.17.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\..\..\..\..\packages\Octokit.0.17.0\lib\net45\Octokit.dll</HintPath>
<Private>True</Private>
......
......@@ -11,6 +11,8 @@ internal sealed class Options
public string RepositoryPath { get; set; }
public RepositoryType SourceRepoType { get; set; }
public string SourceRepoName { get; set; }
public string SourceProject { get; set; }
public string SourceUserId { get; set; }
public string SourceUserName { get; set; }
public string SourcePassword { get; set; }
public string SourceRemoteName { get; set; }
......@@ -27,6 +29,9 @@ internal sealed class Options
private string _destinationRemoteName;
private string _destinationBranchName;
public string DestinationProject { get; set; }
public string DestinationUserId { get; set; }
public string PullRequestBranchSourceRemote
{
get { return _prBranchSourceRemote ?? SourceRemoteName; }
......
......@@ -26,15 +26,19 @@ static int Main(string[] args)
{ "repopath=", "The local path to the repository.", value => options.RepositoryPath = value },
{ "sourcetype=", "The source repository type. Valid values are 'GitHub' and 'VisualStudioOnline'.", value => options.SourceRepoType = (RepositoryType)Enum.Parse(typeof(RepositoryType), value) },
{ "sourcereponame=", "The name of the source repository.", value => options.SourceRepoName = value },
{ "sourceproject=", "The name of the source project. Only needed for VisualStudioOnline repos.", value => options.SourceProject = value },
{ "sourceuserid=", "The source user ID. Only needed for VisualStudioOnline repos.", value => options.SourceUserId = value },
{ "sourceuser=", "The source user name.", value => options.SourceUserName = value },
{ "sourcepassword=", "The source password.", value => options.SourcePassword = value },
{ "sourceremote=", "The source remote name.", value => options.SourceRemoteName = value },
{ "sourcebranch=", "The source branch name.", value => options.SourceBranchName = value },
{ "pushtodestination=", "If true the PR branch will be pushed to the destination repository; if false the PR branch will be pushed to the source.", value => options.PushBranchToDestination = value != null },
{ "prbranchsourceremote=", "The name of the remote the PR should initiate from. Defaults to `sourceremote` parameter.", value => options.PullRequestBranchSourceRemote = value },
{ "destinationtype=", "The destination repository type. Valid values are 'GitHub' and 'VisualStudioOnline'. Defaults to `sourcetype` parameter.", value => options.SourceRepoType = (RepositoryType)Enum.Parse(typeof(RepositoryType), value) },
{ "destinationtype=", "The destination repository type. Valid values are 'GitHub' and 'VisualStudioOnline'. Defaults to `sourcetype` parameter.", value => options.DestinationRepoType = (RepositoryType)Enum.Parse(typeof(RepositoryType), value) },
{ "destinationrepoowner=", "", value => options.DestinationRepoOwner = value },
{ "destinationreponame=", "The name of the destination repository. Defaults to `sourcereponame` parameter.", value => options.DestinationRepoName = value },
{ "destinationproject=", "The name of the destination project. Only needed for VisualStudioOnline repos.", value => options.DestinationProject = value },
{ "destinationuserid=", "The destination user ID. Only needed for VisualStudioOnline repos.", value => options.DestinationUserId = value },
{ "destinationuser=", "The destination user name. Defaults to `sourceuser` parameter.", value => options.DestinationUserName = value },
{ "destinationpassword=", "The destination password. Defaults to `sourcepassword` parameter.", value => options.DestinationPassword = value },
{ "destinationremote=", "The destination remote name. Defaults to `sourceremote` parameter.", value => options.DestinationRemoteName = value },
......@@ -53,8 +57,8 @@ static int Main(string[] args)
return options.IsValid ? 0 : 1;
}
var sourceRepository = RepositoryBase.Create(options.SourceRepoType, options.RepositoryPath, options.SourceRepoName, options.SourceUserName, options.SourcePassword);
var destRepository = RepositoryBase.Create(options.DestinationRepoType, options.RepositoryPath, options.DestinationRepoName, options.DestinationUserName, options.DestinationPassword);
var sourceRepository = RepositoryBase.Create(options.SourceRepoType, options.RepositoryPath, options.SourceRepoName, options.SourceProject, options.SourceUserId, options.SourceUserName, options.SourcePassword, options.SourceRemoteName);
var destRepository = RepositoryBase.Create(options.DestinationRepoType, options.RepositoryPath, options.DestinationRepoName, options.DestinationProject, options.DestinationUserId, options.DestinationUserName, options.DestinationPassword, options.DestinationRemoteName);
new Program(sourceRepository, destRepository, options).RunAsync().GetAwaiter().GetResult();
return 0;
}
......@@ -79,10 +83,12 @@ private Program(RepositoryBase sourceRepository, RepositoryBase destinationRepos
public async Task RunAsync()
{
await _sourceRepo.Initialize();
await _destRepo.Initialize();
// fetch latest sources
WriteLine("Fetching.");
_sourceRepo.FetchAll();
_destRepo.FetchAll();
_sourceRepo.Fetch(_options.PullRequestBranchSourceRemote);
var (prRepo, prRemoteName, prUserName, prPassword) = _options.PushBranchToDestination
? (_destRepo, _options.DestinationRemoteName, _options.DestinationUserName, _options.DestinationPassword)
......@@ -139,7 +145,7 @@ public async Task RunAsync()
}
else
{
await _destRepo.CreatePullRequestAsync(title, _options.DestinationRepoOwner, prBranchName, _options.SourceBranchName, _options.DestinationBranchName);
await _destRepo.CreatePullRequestAsync(title, _options.DestinationRepoOwner, prBranchName, _options.PullRequestBranchSourceRemote, _options.SourceBranchName, _options.DestinationBranchName);
}
}
}
......
# Example
# Examples
Example merge from `dotnet:dev15-rc2` to `dotnet:master` where the credentials used belong to a fictional user `merge-bot`.
## Merge from `https://github.com/dotnet/roslyn`:`dev15-rc2` to `master` where the credentials used belong to a fictional user `merge-bot`.
``` cmd
set username=merge-bot
set password=[password-or-auth-token]
set sourcebranch=dev15-rc2
set destbranch=master
GitMergeBot.exe --repopath=C:\path\to\repo --sourcetype=GitHub --sourcereponame=roslyn --sourceuser=%username% --sourcepassword=%password% --sourceremote=origin --sourcebranch=%sourcebranch% --pushtodestination- --prbranchsourceremote=upstream --destinationrepoowner=dotnet --destinationremote=upstream --destinationbranch=%destbranch%
GitMergeBot.exe --repopath=C:\path\to\roslyn\repo --sourcetype=GitHub --sourcereponame=roslyn --sourceuser=merge-bot --sourcepassword=super-secret-key --sourceremote=origin --sourcebranch=dev15-rc2 --pushtodestination- --prbranchsourceremote=upstream --destinationrepoowner=dotnet --destinationremote=upstream --destinationbranch=master
```
## Merge from `https://github.com/Microsoft/visualfsharp`:`master` to `https://<internal-f#-repository>`:`microbuild` on a VSO instance where the credentials belong to a fictional user `merge-bot`.
``` cmd
GitMergeBot.exe --repopath=C:\path\to\fsharp\repo --sourcetype=GitHub --sourcereponame=visualfsharp --sourceuser=merge-bot --sourcepassword=super-secret-key --sourceremote=origin --sourcebranch=master --pushtodestination+ --prbranchsourceremote=upstream --destinationtype=VisualStudioOnline --destinationreponame=FSharp --destinationproject=DevDiv --destinationuserid=[GUID] --destinationuser= --destinationpassword=super-secret-key --destinationremote=vso --destinationbranch=microbuild
```
......@@ -21,30 +21,40 @@ protected RepositoryBase(string path, string repoName, string userName, string p
Password = password;
}
public virtual Task Initialize()
{
return Task.CompletedTask;
}
public abstract Task<bool> ShouldMakePullRequestAsync(string title);
public abstract Task CreatePullRequestAsync(string title, string destinationOwner, string pullRequestBranch, string sourceBranch, string destinationBranch);
public abstract Task CreatePullRequestAsync(string title, string destinationOwner, string pullRequestBranch, string prBranchSourceRemote, string sourceBranch, string destinationBranch);
protected void WriteDebugLine(string line)
{
Console.WriteLine("Debug: " + line);
}
public void FetchAll()
public void Fetch(string remoteName)
{
var fetchOptions = new FetchOptions()
{
foreach (var remote in Repository.Network.Remotes)
CredentialsProvider = (url, usernameFromUrl, types) => new UsernamePasswordCredentials()
{
Repository.Fetch(remote.Name);
Username = UserName,
Password = Password
}
};
Repository.Fetch(remoteName, fetchOptions);
}
public static RepositoryBase Create(RepositoryType type, string path, string repoName, string userName, string password)
public static RepositoryBase Create(RepositoryType type, string path, string repoName, string project, string userId, string userName, string password, string remoteName)
{
switch (type)
{
case RepositoryType.GitHub:
return new GitHubRepository(path, repoName, userName, password);
case RepositoryType.VisualStudioOnline:
return new VisualStudioOnlineRepository(path, repoName, userName, password);
return new VisualStudioOnlineRepository(path, repoName, project, userId, userName, password, remoteName);
default:
throw new InvalidOperationException("Unknown repository type.");
}
......
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
namespace GitMergeBot
{
internal sealed class VisualStudioOnlineRepository : RepositoryBase
{
public VisualStudioOnlineRepository(string path, string repoName, string userName, string password)
private const string ApiVersion = "3.0";
private HttpClient _client;
private string _project;
private string _remoteName;
private string _repositoryId;
private string _userId;
public VisualStudioOnlineRepository(string path, string repoName, string project, string userId, string userName, string password, string remoteName)
: base(path, repoName, userName, password)
{
_project = project;
_remoteName = remoteName;
_userId = userId;
var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{userName}:{password}"));
var remote = Repository.Network.Remotes[remoteName];
var remoteUri = new Uri(remote.Url);
_client = new HttpClient();
_client.BaseAddress = new Uri($"{remoteUri.Scheme}://{remoteUri.Host}");
_client.DefaultRequestHeaders.Accept.Clear();
_client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", credentials);
}
public override Task<bool> ShouldMakePullRequestAsync(string title)
public override async Task Initialize()
{
throw new NotImplementedException();
// find the repository ID
// https://www.visualstudio.com/en-us/docs/integrate/api/git/repositories#get-a-list-of-repositories
var repositories = await GetJsonAsync($"DefaultCollection/{_project}/_apis/git/repositories?api-version={ApiVersion}");
_repositoryId = (string)repositories["value"].Single(r => r?["name"].Type == JTokenType.String && (string)r["name"] == RepositoryName)["id"];
}
public override Task CreatePullRequestAsync(string title, string destinationOwner, string pullRequestBranch, string sourceBranch, string destinationBranch)
public override async Task<bool> ShouldMakePullRequestAsync(string title)
{
// https://www.visualstudio.com/en-us/docs/integrate/api/git/pull-requests/pull-requests#get-a-list-of-pull-requests-in-the-repository
var foundMatch = false;
var result = await GetJsonAsync($"DefaultCollection/_apis/git/repositories/{_repositoryId}/pullRequests?api-version={ApiVersion}&creatorId={_userId}");
var pullRequests = (JArray)result["value"];
foreach (JObject pr in pullRequests)
{
throw new NotImplementedException();
if (pr?["repository"]?["name"].Type == JTokenType.String && (string)pr["repository"]["name"] == RepositoryName)
{
var prTitle = (string)pr["title"];
Console.WriteLine($" Open PR: {prTitle}");
foundMatch |= prTitle == title;
}
}
return !foundMatch;
}
public override async Task CreatePullRequestAsync(string title, string destinationOwner, string pullRequestBranch, string prBranchSourceRemote, string sourceBranch, string destinationBranch)
{
// https://www.visualstudio.com/en-us/docs/integrate/api/git/pull-requests/pull-requests#create-a-pull-request
var prMessage = $@"
This is an automatically generated pull request from {sourceBranch} into {destinationBranch}.
``` bash
git remote add {_remoteName} {Repository.Network.Remotes[_remoteName].Url}
git fetch --all
git checkout {pullRequestBranch}
git reset --hard {_remoteName}/{destinationBranch}
git merge {prBranchSourceRemote}/{sourceBranch}
# Fix merge conflicts
git commit
git push {pullRequestBranch} --force
```
Once all conflicts are resolved and all the tests pass, you are free to merge the pull request.
".Trim();
var request = new JObject()
{
["sourceRefName"] = $"refs/heads/{pullRequestBranch}",
["targetRefName"] = $"refs/heads/{destinationBranch}",
["title"] = title,
["description"] = prMessage,
["reviewers"] = new JArray() // no required reviewers, but necessary for the request
};
var result = await GetJsonAsync(
$"DefaultCollection/_apis/git/repositories/{_repositoryId}/pullRequests?api-version={ApiVersion}",
body: request,
method: "POST");
var pullRequestId = (string)result["pullRequestId"];
// mark the PR to auto complete
// https://www.visualstudio.com/en-us/docs/integrate/api/git/pull-requests/pull-requests#auto-complete
var autoCompleteRequest = new JObject()
{
["autoCompleteSetBy"] = new JObject()
{
["id"] = _userId
},
["completionOptions"] = new JObject()
{
["deleteSourceBranch"] = true,
["mergeCommitMessage"] = $"Pull request #{pullRequestId} auto-completed after passing checks.",
["squashMerge"] = false
}
};
result = await GetJsonAsync(
$"DefaultCollection/_apis/git/repositories/{_repositoryId}/pullRequests/{pullRequestId}?api-version={ApiVersion}",
body: autoCompleteRequest,
method: "PATCH");
}
private async Task<JObject> GetJsonAsync(string requestUri, JObject body = null, string method = "GET")
{
HttpResponseMessage response;
if (body == null)
{
response = await _client.GetAsync(requestUri);
}
else
{
var requestMessage = new HttpRequestMessage(new HttpMethod(method), requestUri);
requestMessage.Content = new ByteArrayContent(Encoding.ASCII.GetBytes(body.ToString()));
requestMessage.Content.Headers.Add("Content-Type", "application/json");
response = await _client.SendAsync(requestMessage);
}
var result = await response.Content.ReadAsStringAsync();
return JObject.Parse(result);
}
}
}
......@@ -3,6 +3,7 @@
<package id="LibGit2Sharp" version="0.22.0" targetFramework="net46" />
<package id="LibGit2Sharp.NativeBinaries" version="1.0.129" targetFramework="net46" />
<package id="Mono.Options" version="1.1" targetFramework="net452" />
<package id="Newtonsoft.Json" version="9.0.1" targetFramework="net46" />
<package id="Octokit" version="0.17.0" targetFramework="net452" />
<package id="System.ValueTuple" version="4.3.0-preview1-24530-04" targetFramework="net46" />
</packages>
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册