Skip to content
This repository was archived by the owner on Jun 21, 2023. It is now read-only.

Commit 69ff01a

Browse files
authored
Merge pull request #194 from github/feature/gist-support
Feature: Gist support
2 parents 5b466de + db64eaf commit 69ff01a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+2086
-106
lines changed

.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -238,4 +238,4 @@ WiX.Toolset.DummyFile.txt
238238
nunit-UnitTests.xml
239239
nunit-TrackingCollectionTests.xml
240240
GitHubVS.sln.DotSettings
241-
**/generated/*.cs
241+
**/generated/*.cs

src/GitHub.App/Api/ApiClient.cs

+38-6
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,27 @@
1313
using Octokit;
1414
using Octokit.Reactive;
1515
using ReactiveUI;
16+
using System.Threading.Tasks;
17+
using System.Reactive.Threading.Tasks;
18+
using Octokit.Internal;
19+
using System.Collections.Generic;
20+
using GitHub.Models;
1621

1722
namespace GitHub.Api
1823
{
1924
public partial class ApiClient : IApiClient
2025
{
21-
static readonly Logger log = LogManager.GetCurrentClassLogger();
22-
26+
const string ScopesHeader = "X-OAuth-Scopes";
2327
const string ProductName = Info.ApplicationInfo.ApplicationDescription;
28+
static readonly Logger log = LogManager.GetCurrentClassLogger();
29+
static readonly Uri userEndpoint = new Uri("user", UriKind.Relative);
2430

2531
readonly IObservableGitHubClient gitHubClient;
2632
// There are two sets of authorization scopes, old and new:
2733
// The old scopes must be used by older versions of Enterprise that don't support the new scopes:
28-
readonly string[] oldAuthorizationScopes = { "user", "repo" };
34+
readonly string[] oldAuthorizationScopes = { "user", "repo", "gist" };
2935
// These new scopes include write:public_key, which allows us to add public SSH keys to an account:
30-
readonly string[] newAuthorizationScopes = { "user", "repo", "write:public_key" };
36+
readonly string[] newAuthorizationScopes = { "user", "repo", "gist", "write:public_key" };
3137
readonly static Lazy<string> lazyNote = new Lazy<string>(() => ProductName + " on " + GetMachineNameSafe());
3238
readonly static Lazy<string> lazyFingerprint = new Lazy<string>(GetFingerprint);
3339

@@ -52,9 +58,35 @@ public IObservable<Repository> CreateRepository(NewRepository repository, string
5258
return (isUser ? client.Create(repository) : client.Create(login, repository));
5359
}
5460

55-
public IObservable<User> GetUser()
61+
public IObservable<Gist> CreateGist(NewGist newGist)
62+
{
63+
return gitHubClient.Gist.Create(newGist);
64+
}
65+
66+
public IObservable<UserAndScopes> GetUser()
5667
{
57-
return gitHubClient.User.Current();
68+
return GetUserInternal().ToObservable();
69+
}
70+
71+
async Task<UserAndScopes> GetUserInternal()
72+
{
73+
var response = await gitHubClient.Connection.Get<User>(
74+
userEndpoint, null, null).ConfigureAwait(false);
75+
var scopes = default(string[]);
76+
77+
if (response.HttpResponse.Headers.ContainsKey(ScopesHeader))
78+
{
79+
scopes = response.HttpResponse.Headers[ScopesHeader]
80+
.Split(',')
81+
.Select(x => x.Trim())
82+
.ToArray();
83+
}
84+
else
85+
{
86+
log.Error($"Error reading scopes: /user succeeded but {ScopesHeader} was not present.");
87+
}
88+
89+
return new UserAndScopes(response.Body, scopes);
5890
}
5991

6092
public IObservable<ApplicationAuthorization> GetOrCreateApplicationAuthenticationCode(

src/GitHub.App/Controllers/UIController.cs

+59-7
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,9 @@ public UIController(IUIProvider uiProvider, IRepositoryHosts hosts, IUIFactory f
170170

171171
uiStateMachine = new StateMachineType(UIViewType.None);
172172
triggers = new Dictionary<Trigger, StateMachineType.TriggerWithParameters<ViewWithData>>();
173+
173174
ConfigureUIHandlingStates();
175+
174176
}
175177

176178
public IObservable<LoadData> SelectFlow(UIControllerFlow choice)
@@ -257,12 +259,12 @@ public void Start([AllowNull] IConnection conn)
257259

258260
public void Jump(ViewWithData where)
259261
{
260-
Debug.Assert(where.Flow == mainFlow, "Jump called for flow " + where.Flow + " but this is " + mainFlow);
261-
if (where.Flow != mainFlow)
262+
Debug.Assert(where.ActiveFlow == mainFlow, "Jump called for flow " + where.ActiveFlow + " but this is " + mainFlow);
263+
if (where.ActiveFlow != mainFlow)
262264
return;
263265

264266
requestedTarget = where;
265-
if (activeFlow == where.Flow)
267+
if (activeFlow == where.ActiveFlow)
266268
Fire(Trigger.Next, where);
267269
}
268270

@@ -295,8 +297,7 @@ void ConfigureUIHandlingStates()
295297
.OnEntry(tr => stopping = false)
296298
.PermitDynamic(Trigger.Next, () =>
297299
{
298-
var loggedIn = connection != null && hosts.LookupHost(connection.HostAddress).IsLoggedIn;
299-
activeFlow = loggedIn ? mainFlow : UIControllerFlow.Authentication;
300+
activeFlow = SelectActiveFlow();
300301
return Go(Trigger.Next);
301302
})
302303
.PermitDynamic(Trigger.Finish, () => Go(Trigger.Finish));
@@ -352,6 +353,18 @@ void ConfigureUIHandlingStates()
352353
.PermitDynamic(Trigger.Cancel, () => Go(Trigger.Cancel))
353354
.PermitDynamic(Trigger.Finish, () => Go(Trigger.Finish));
354355

356+
uiStateMachine.Configure(UIViewType.Gist)
357+
.OnEntry(tr => RunView(UIViewType.Gist, CalculateDirection(tr)))
358+
.PermitDynamic(Trigger.Next, () => Go(Trigger.Next))
359+
.PermitDynamic(Trigger.Cancel, () => Go(Trigger.Cancel))
360+
.PermitDynamic(Trigger.Finish, () => Go(Trigger.Finish));
361+
362+
uiStateMachine.Configure(UIViewType.LogoutRequired)
363+
.OnEntry(tr => RunView(UIViewType.LogoutRequired, CalculateDirection(tr)))
364+
.PermitDynamic(Trigger.Next, () => Go(Trigger.Next))
365+
.PermitDynamic(Trigger.Cancel, () => Go(Trigger.Cancel))
366+
.PermitDynamic(Trigger.Finish, () => Go(Trigger.Finish));
367+
355368
uiStateMachine.Configure(UIViewType.End)
356369
.OnEntryFrom(Trigger.Cancel, () => End(false))
357370
.OnEntryFrom(Trigger.Next, () => End(true))
@@ -518,6 +531,43 @@ void ConfigureLogicStates()
518531
logic.Configure(UIViewType.End)
519532
.Permit(Trigger.Next, UIViewType.None);
520533
machines.Add(UIControllerFlow.PullRequests, logic);
534+
535+
// gist flow
536+
logic = new StateMachine<UIViewType, Trigger>(UIViewType.None);
537+
logic.Configure(UIViewType.None)
538+
.Permit(Trigger.Next, UIViewType.Gist)
539+
.Permit(Trigger.Finish, UIViewType.End);
540+
logic.Configure(UIViewType.Gist)
541+
.Permit(Trigger.Next, UIViewType.End)
542+
.Permit(Trigger.Cancel, UIViewType.End)
543+
.Permit(Trigger.Finish, UIViewType.End);
544+
logic.Configure(UIViewType.End)
545+
.Permit(Trigger.Next, UIViewType.None);
546+
machines.Add(UIControllerFlow.Gist, logic);
547+
548+
// logout required flow
549+
logic = new StateMachine<UIViewType, Trigger>(UIViewType.None);
550+
logic.Configure(UIViewType.None)
551+
.Permit(Trigger.Next, UIViewType.LogoutRequired)
552+
.Permit(Trigger.Finish, UIViewType.End);
553+
logic.Configure(UIViewType.LogoutRequired)
554+
.Permit(Trigger.Next, UIViewType.End)
555+
.Permit(Trigger.Cancel, UIViewType.End)
556+
.Permit(Trigger.Finish, UIViewType.End);
557+
logic.Configure(UIViewType.End)
558+
.Permit(Trigger.Next, UIViewType.None);
559+
machines.Add(UIControllerFlow.LogoutRequired, logic);
560+
}
561+
562+
UIControllerFlow SelectActiveFlow()
563+
{
564+
var host = connection != null ? hosts.LookupHost(connection.HostAddress) : null;
565+
var loggedIn = host?.IsLoggedIn ?? false;
566+
if (!loggedIn || mainFlow != UIControllerFlow.Gist)
567+
return loggedIn ? mainFlow : UIControllerFlow.Authentication;
568+
569+
var supportsGist = host?.SupportsGist ?? false;
570+
return supportsGist ? mainFlow : UIControllerFlow.LogoutRequired;
521571
}
522572

523573
static LoadDirection CalculateDirection(StateMachineType.Transition tr)
@@ -580,12 +630,14 @@ void RunView(UIViewType viewType, LoadDirection direction, ViewWithData arg = nu
580630
requestedTarget = null;
581631
}
582632

633+
if (arg == null)
634+
arg = new ViewWithData { ActiveFlow = activeFlow, MainFlow = mainFlow, ViewType = viewType };
583635
bool firstTime = CreateViewAndViewModel(viewType, arg);
584636
var view = GetObjectsForFlow(activeFlow)[viewType].View;
585637
transition.OnNext(new LoadData
586638
{
587639
View = view,
588-
Data = arg ?? new ViewWithData { Flow = activeFlow, ViewType = viewType },
640+
Data = arg,
589641
Direction = direction
590642
});
591643

@@ -660,7 +712,7 @@ void SetupView(UIViewType viewType, IView view)
660712
/// </summary>
661713
/// <param name="viewType"></param>
662714
/// <returns>true if the View/ViewModel didn't exist and had to be created</returns>
663-
bool CreateViewAndViewModel(UIViewType viewType, [AllowNull]ViewWithData data = null)
715+
bool CreateViewAndViewModel(UIViewType viewType, ViewWithData data = null)
664716
{
665717
var list = GetObjectsForFlow(activeFlow);
666718
if (viewType == UIViewType.Login)

src/GitHub.App/GitHub.App.csproj

+4
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
<Reference Include="Microsoft.VisualStudio.ComponentModelHost, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
5555
<Reference Include="Microsoft.VisualStudio.Shell.14.0, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
5656
<Reference Include="Microsoft.VisualStudio.Shell.Immutable.10.0" />
57+
<Reference Include="Microsoft.VisualStudio.TextManager.Interop, Version=7.1.40304.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
5758
<Reference Include="Newtonsoft.Json, Version=6.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
5859
<SpecificVersion>False</SpecificVersion>
5960
<HintPath>..\..\packages\Newtonsoft.Json.6.0.8\lib\net45\Newtonsoft.Json.dll</HintPath>
@@ -180,6 +181,7 @@
180181
<Compile Include="Services\IEnterpriseProbe.cs" />
181182
<Compile Include="Factories\RepositoryHostFactory.cs" />
182183
<Compile Include="Services\RepositoryCreationService.cs" />
184+
<Compile Include="Services\GistPublishService.cs" />
183185
<Compile Include="Services\RepositoryPublishService.cs" />
184186
<Compile Include="Services\SerializedObjectProvider.cs" />
185187
<Compile Include="Services\StandardUserErrors.cs" />
@@ -190,6 +192,8 @@
190192
<Compile Include="UserErrors\PrivateRepositoryQuotaExceededUserError.cs" />
191193
<Compile Include="ViewModels\BaseViewModel.cs" />
192194
<Compile Include="Models\ConnectionRepositoryHostMap.cs" />
195+
<Compile Include="ViewModels\GistCreationViewModel.cs" />
196+
<Compile Include="ViewModels\LogoutRequiredViewModel.cs" />
193197
<Compile Include="ViewModels\PullRequestCreationViewModel.cs" />
194198
<Compile Include="ViewModels\PullRequestDetailViewModel.cs" />
195199
<Compile Include="ViewModels\PullRequestListViewModel.cs" />

src/GitHub.App/Models/ConnectionRepositoryHostMap.cs

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System.ComponentModel.Composition;
22
using GitHub.Models;
33
using GitHub.Services;
4+
using GitHub.Extensions;
45

56
namespace GitHub.ViewModels
67
{

src/GitHub.App/Models/DisconnectedRepositoryHosts.cs

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public DisconnectedRepositoryHost()
2626
[AllowNull]
2727
public bool IsLoggedIn { get; private set; }
2828
public bool IsLoggingIn { get; private set; }
29+
public bool SupportsGist { get; private set; }
2930
public ReactiveList<IAccount> Organizations { get; private set; }
3031
public ReactiveList<IAccount> Accounts { get; private set; }
3132
public string Title { get; private set; }

src/GitHub.App/Models/RepositoryHost.cs

+25-26
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,17 @@
1414
using NLog;
1515
using Octokit;
1616
using ReactiveUI;
17+
using System.Linq;
18+
using System.Reactive.Threading.Tasks;
19+
using System.Collections.Generic;
1720

1821
namespace GitHub.Models
1922
{
2023
[DebuggerDisplay("{DebuggerDisplay,nq}")]
2124
public class RepositoryHost : ReactiveObject, IRepositoryHost
2225
{
2326
static readonly Logger log = LogManager.GetCurrentClassLogger();
24-
static readonly AccountCacheItem unverifiedUser = new AccountCacheItem();
27+
static readonly UserAndScopes unverifiedUser = new UserAndScopes(null, null);
2528

2629
readonly ITwoFactorChallengeHandler twoFactorChallengeHandler;
2730
readonly HostAddress hostAddress;
@@ -58,27 +61,22 @@ public bool IsLoggedIn
5861
private set { this.RaiseAndSetIfChanged(ref isLoggedIn, value); }
5962
}
6063

64+
public bool SupportsGist { get; private set; }
65+
6166
public string Title { get; private set; }
6267

6368
[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope")]
6469
public IObservable<AuthenticationResult> LogInFromCache()
6570
{
6671
return GetUserFromApi()
6772
.ObserveOn(RxApp.MainThreadScheduler)
68-
.Catch<AccountCacheItem, Exception>(ex =>
73+
.Catch<UserAndScopes, Exception>(ex =>
6974
{
7075
if (ex is AuthorizationException)
7176
{
7277
log.Warn("Got an authorization exception", ex);
73-
return Observable.Return<AccountCacheItem>(null);
7478
}
75-
return ModelService.GetUserFromCache()
76-
.Catch<AccountCacheItem, Exception>(e =>
77-
{
78-
log.Warn("User does not exist in cache", e);
79-
return Observable.Return<AccountCacheItem>(null);
80-
})
81-
.ObserveOn(RxApp.MainThreadScheduler);
79+
return Observable.Return<UserAndScopes>(null);
8280
})
8381
.SelectMany(LoginWithApiUser)
8482
.PublishAsync();
@@ -121,14 +119,14 @@ public IObservable<AuthenticationResult> LogIn(string usernameOrEmail, string pa
121119
.SelectMany(fingerprint => ApiClient.GetOrCreateApplicationAuthenticationCode(interceptingTwoFactorChallengeHandler))
122120
.SelectMany(saveAuthorizationToken)
123121
.SelectMany(_ => GetUserFromApi())
124-
.Catch<AccountCacheItem, ApiException>(firstTryEx =>
122+
.Catch<UserAndScopes, ApiException>(firstTryEx =>
125123
{
126124
var exception = firstTryEx as AuthorizationException;
127125
if (isEnterprise
128126
&& exception != null
129127
&& exception.Message == "Bad credentials")
130128
{
131-
return Observable.Throw<AccountCacheItem>(exception);
129+
return Observable.Throw<UserAndScopes>(exception);
132130
}
133131

134132
// If the Enterprise host doesn't support the write:public_key scope, it'll return a 422.
@@ -164,9 +162,9 @@ public IObservable<AuthenticationResult> LogIn(string usernameOrEmail, string pa
164162
.SelectMany(_ => GetUserFromApi());
165163
}
166164

167-
return Observable.Throw<AccountCacheItem>(firstTryEx);
165+
return Observable.Throw<UserAndScopes>(firstTryEx);
168166
})
169-
.Catch<AccountCacheItem, ApiException>(retryEx =>
167+
.Catch<UserAndScopes, ApiException>(retryEx =>
170168
{
171169
// Older Enterprise hosts either don't have the API end-point to PUT an authorization, or they
172170
// return 422 because they haven't white-listed our client ID. In that case, we just ignore
@@ -179,10 +177,10 @@ public IObservable<AuthenticationResult> LogIn(string usernameOrEmail, string pa
179177
return GetUserFromApi();
180178

181179
// Other errors are "real" so we pass them along:
182-
return Observable.Throw<AccountCacheItem>(retryEx);
180+
return Observable.Throw<UserAndScopes>(retryEx);
183181
})
184182
.ObserveOn(RxApp.MainThreadScheduler)
185-
.Catch<AccountCacheItem, Exception>(ex =>
183+
.Catch<UserAndScopes, Exception>(ex =>
186184
{
187185
// If we get here, we have an actual login failure:
188186
if (ex is TwoFactorChallengeFailedException)
@@ -191,9 +189,9 @@ public IObservable<AuthenticationResult> LogIn(string usernameOrEmail, string pa
191189
}
192190
if (ex is AuthorizationException)
193191
{
194-
return Observable.Return(default(AccountCacheItem));
192+
return Observable.Return(default(UserAndScopes));
195193
}
196-
return Observable.Throw<AccountCacheItem>(ex);
194+
return Observable.Throw<UserAndScopes>(ex);
197195
})
198196
.SelectMany(LoginWithApiUser)
199197
.PublishAsync();
@@ -224,22 +222,23 @@ public IObservable<Unit> LogOut()
224222
});
225223
}
226224

227-
static IObservable<AuthenticationResult> GetAuthenticationResultForUser(AccountCacheItem account)
225+
static IObservable<AuthenticationResult> GetAuthenticationResultForUser(UserAndScopes account)
228226
{
229227
return Observable.Return(account == null ? AuthenticationResult.CredentialFailure
230228
: account == unverifiedUser
231229
? AuthenticationResult.VerificationFailure
232230
: AuthenticationResult.Success);
233231
}
234232

235-
IObservable<AuthenticationResult> LoginWithApiUser(AccountCacheItem user)
233+
IObservable<AuthenticationResult> LoginWithApiUser(UserAndScopes userAndScopes)
236234
{
237-
return GetAuthenticationResultForUser(user)
235+
return GetAuthenticationResultForUser(userAndScopes)
238236
.SelectMany(result =>
239237
{
240238
if (result.IsSuccess())
241239
{
242-
return ModelService.InsertUser(user).Select(_ => result);
240+
var accountCacheItem = new AccountCacheItem(userAndScopes.User);
241+
return ModelService.InsertUser(accountCacheItem).Select(_ => result);
243242
}
244243

245244
if (result == AuthenticationResult.VerificationFailure)
@@ -254,19 +253,19 @@ IObservable<AuthenticationResult> LoginWithApiUser(AccountCacheItem user)
254253
if (result.IsSuccess())
255254
{
256255
IsLoggedIn = true;
256+
SupportsGist = userAndScopes.Scopes?.Contains("gist") ?? true;
257257
}
258258

259259
log.Info("Log in from cache for login '{0}' to host '{1}' {2}",
260-
user != null ? user.Login : "(null)",
260+
userAndScopes?.User?.Login ?? "(null)",
261261
hostAddress.ApiUri,
262262
result.IsSuccess() ? "SUCCEEDED" : "FAILED");
263263
});
264264
}
265265

266-
IObservable<AccountCacheItem> GetUserFromApi()
266+
IObservable<UserAndScopes> GetUserFromApi()
267267
{
268-
return Observable.Defer(() => ApiClient.GetUser().WhereNotNull()
269-
.Select(user => new AccountCacheItem(user)));
268+
return Observable.Defer(() => ApiClient.GetUser());
270269
}
271270

272271
bool disposed;

0 commit comments

Comments
 (0)