import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:dots_indicator/dots_indicator.dart'; import 'package:function_types/function_types.dart'; import 'package:git_bindings/git_bindings.dart'; import 'package:path/path.dart' as p; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:gitjournal/analytics.dart'; import 'package:gitjournal/apis/githost_factory.dart'; import 'package:gitjournal/error_reporting.dart'; import 'package:gitjournal/settings.dart'; import 'package:gitjournal/setup/repo_selector.dart'; import 'package:gitjournal/state_container.dart'; import 'package:gitjournal/utils.dart'; import 'package:gitjournal/utils/logger.dart'; import 'autoconfigure.dart'; import 'button.dart'; import 'clone_url.dart'; import 'loading_error.dart'; import 'sshkey.dart'; class GitHostSetupScreen extends StatefulWidget { final String repoFolderName; final Func1 onCompletedFunction; GitHostSetupScreen(this.repoFolderName, this.onCompletedFunction); @override GitHostSetupScreenState createState() { return GitHostSetupScreenState(); } } enum PageChoice0 { Unknown, KnownProvider, CustomProvider } enum PageChoice1 { Unknown, Manual, Auto } enum KeyGenerationChoice { Unknown, AutoGenerated, UserProvided } class GitHostSetupScreenState extends State { var _pageCount = 1; var _pageChoice = [ PageChoice0.Unknown, PageChoice1.Unknown, ]; var _keyGenerationChoice = KeyGenerationChoice.Unknown; var _gitHostType = GitHostType.Unknown; GitHost _gitHost; GitHostRepo _gitHostRepo; String _autoConfigureMessage = ""; String _autoConfigureErrorMessage = ""; var _gitCloneUrl = ""; var gitCloneErrorMessage = ""; var publicKey = ""; var pageController = PageController(); int _currentPageIndex = 0; Widget _buildPage(BuildContext context, int pos) { assert(_pageCount >= 1); if (pos == 0) { return GitHostChoicePage( onKnownGitHost: (GitHostType gitHostType) { setState(() { _gitHostType = gitHostType; gitCloneErrorMessage = ""; _autoConfigureErrorMessage = ""; _autoConfigureMessage = ""; _pageChoice[0] = PageChoice0.KnownProvider; _pageCount = pos + 2; _nextPage(); }); }, onCustomGitHost: () { setState(() { _pageChoice[0] = PageChoice0.CustomProvider; _pageCount = pos + 2; _nextPage(); }); }, ); } if (pos == 1) { assert(_pageChoice[0] != PageChoice0.Unknown); if (_pageChoice[0] == PageChoice0.CustomProvider) { return GitCloneUrlPage( doneFunction: (String sshUrl) { setState(() { _gitCloneUrl = sshUrl; _pageCount = pos + 2; _nextPage(); }); }, initialValue: _gitCloneUrl, ); } return GitHostAutoConfigureChoicePage( onDone: (GitHostSetupType setupType) { if (setupType == GitHostSetupType.Manual) { setState(() { _pageCount = pos + 2; _pageChoice[1] = PageChoice1.Manual; _nextPage(); }); } else if (setupType == GitHostSetupType.Auto) { setState(() { _pageCount = pos + 2; _pageChoice[1] = PageChoice1.Auto; _nextPage(); }); } }, ); } if (pos == 2) { if (_pageChoice[0] == PageChoice0.CustomProvider) { return GitHostSetupKeyChoice( onGenerateKeys: () { setState(() { _keyGenerationChoice = KeyGenerationChoice.AutoGenerated; _pageCount = pos + 2; _nextPage(); _generateSshKey(context); }); }, onUserProvidedKeys: () { setState(() { _keyGenerationChoice = KeyGenerationChoice.UserProvided; _pageCount = pos + 2; _nextPage(); }); }, ); } assert(_pageChoice[1] != PageChoice1.Unknown); if (_pageChoice[1] == PageChoice1.Manual) { return GitCloneUrlKnownProviderPage( doneFunction: (String sshUrl) { setState(() { _pageCount = pos + 2; _gitCloneUrl = sshUrl; _nextPage(); }); }, launchCreateUrlPage: _launchCreateRepoPage, gitHostType: _gitHostType, initialValue: _gitCloneUrl, ); } else if (_pageChoice[1] == PageChoice1.Auto) { return GitHostSetupAutoConfigure( gitHostType: _gitHostType, onDone: (GitHost gitHost) { setState(() { _gitHost = gitHost; _pageCount = pos + 2; _nextPage(); }); }, ); } } if (pos == 3) { if (_pageChoice[0] == PageChoice0.CustomProvider) { assert(_keyGenerationChoice != KeyGenerationChoice.Unknown); if (_keyGenerationChoice == KeyGenerationChoice.AutoGenerated) { return GitHostSetupSshKeyUnknownProvider( doneFunction: () { setState(() { _pageCount = pos + 2; _nextPage(); _startGitClone(context); }); }, regenerateFunction: () { setState(() { publicKey = ""; }); _generateSshKey(context); }, publicKey: publicKey, copyKeyFunction: _copyKeyToClipboard, ); } else if (_keyGenerationChoice == KeyGenerationChoice.UserProvided) { return GitHostUserProvidedKeys( doneFunction: (String publicKey, String privateKey) async { await setSshKeys(publicKey: publicKey, privateKey: privateKey); setState(() { this.publicKey = publicKey; _pageCount = pos + 2; _nextPage(); _startGitClone(context); }); }, ); } } if (_pageChoice[1] == PageChoice1.Manual) { return GitHostSetupKeyChoice( onGenerateKeys: () { setState(() { _keyGenerationChoice = KeyGenerationChoice.AutoGenerated; _pageCount = pos + 2; _nextPage(); _generateSshKey(context); }); }, onUserProvidedKeys: () { setState(() { _keyGenerationChoice = KeyGenerationChoice.UserProvided; _pageCount = pos + 2; _nextPage(); }); }, ); } else if (_pageChoice[1] == PageChoice1.Auto) { return GitHostSetupRepoSelector( gitHost: _gitHost, onDone: (GitHostRepo repo) { setState(() { _gitHostRepo = repo; _pageCount = pos + 2; _nextPage(); _completeAutoConfigure(); }); }, ); } assert(false); } if (pos == 4) { if (_pageChoice[0] == PageChoice0.CustomProvider) { return GitHostSetupLoadingErrorPage( loadingMessage: "Cloning ...", errorMessage: gitCloneErrorMessage, ); } if (_pageChoice[1] == PageChoice1.Manual) { assert(_keyGenerationChoice != KeyGenerationChoice.Unknown); if (_keyGenerationChoice == KeyGenerationChoice.AutoGenerated) { return GitHostSetupSshKeyKnownProvider( doneFunction: () { setState(() { _pageCount = 6; _nextPage(); _startGitClone(context); }); }, regenerateFunction: () { setState(() { publicKey = ""; }); _generateSshKey(context); }, publicKey: publicKey, copyKeyFunction: _copyKeyToClipboard, openDeployKeyPage: _launchDeployKeyPage, ); } else if (_keyGenerationChoice == KeyGenerationChoice.UserProvided) { return GitHostUserProvidedKeys( doneFunction: (String publicKey, String privateKey) async { await setSshKeys(publicKey: publicKey, privateKey: privateKey); setState(() { this.publicKey = publicKey; _pageCount = pos + 2; _nextPage(); _startGitClone(context); }); }, ); } } else if (_pageChoice[1] == PageChoice1.Auto) { return GitHostSetupLoadingErrorPage( loadingMessage: _autoConfigureMessage, errorMessage: _autoConfigureErrorMessage, ); } } if (pos == 5) { return GitHostSetupLoadingErrorPage( loadingMessage: "Cloning ...", errorMessage: gitCloneErrorMessage, ); } assert(_pageChoice[0] != PageChoice0.CustomProvider); assert(false, "Pos is $pos"); return null; } @override Widget build(BuildContext context) { var pageView = PageView.builder( controller: pageController, itemBuilder: _buildPage, itemCount: _pageCount, onPageChanged: (int pageNum) { setState(() { _currentPageIndex = pageNum; _pageCount = _currentPageIndex + 1; }); }, ); var body = Container( width: double.infinity, height: double.infinity, child: Stack( alignment: FractionalOffset.bottomCenter, children: [ pageView, DotsIndicator( dotsCount: _pageCount, position: _currentPageIndex, decorator: DotsDecorator( activeColor: Theme.of(context).primaryColorDark, ), ), ], ), padding: const EdgeInsets.all(16.0), ); var scaffold = Scaffold( body: Stack( children: [ body, if (Platform.isIOS) SafeArea( child: InkWell( child: Container( child: const Icon(Icons.arrow_back, size: 32.0), padding: const EdgeInsets.all(8.0), ), onTap: () => Navigator.of(context).pop(), ), ), ], ), ); return WillPopScope( onWillPop: () async { if (_currentPageIndex != 0) { pageController.previousPage( duration: const Duration(milliseconds: 200), curve: Curves.easeIn, ); return false; } return true; }, child: scaffold, ); } void _nextPage() { pageController.nextPage( duration: const Duration(milliseconds: 200), curve: Curves.easeIn, ); } void _generateSshKey(BuildContext context) { if (publicKey.isNotEmpty) { return; } var comment = "GitJournal-" + Platform.operatingSystem + "-" + DateTime.now().toIso8601String().substring(0, 10); // only the date generateSSHKeys(comment: comment).then((String publicKey) { setState(() { this.publicKey = publicKey; Log.d("PublicKey: " + publicKey); _copyKeyToClipboard(context); }); }); } void _copyKeyToClipboard(BuildContext context) { Clipboard.setData(ClipboardData(text: publicKey)); showSnackbar(context, "Public Key copied to Clipboard"); } void _launchDeployKeyPage() async { var canLaunch = _gitCloneUrl.startsWith("git@github.com:") || _gitCloneUrl.startsWith("git@gitlab.com:"); if (!canLaunch) { return; } var lastIndex = _gitCloneUrl.lastIndexOf(".git"); if (lastIndex == -1) { lastIndex = _gitCloneUrl.length; } var repoName = _gitCloneUrl.substring(_gitCloneUrl.lastIndexOf(":") + 1, lastIndex); final gitHubUrl = 'https://github.com/' + repoName + '/settings/keys/new'; final gitLabUrl = 'https://gitlab.com/' + repoName + '/-/settings/repository/#js-deploy-keys-settings'; try { if (_gitCloneUrl.startsWith("git@github.com:")) { Log.i("Launching $gitHubUrl"); await launch(gitHubUrl); } else if (_gitCloneUrl.startsWith("git@gitlab.com:")) { Log.i("Launching $gitLabUrl"); await launch(gitLabUrl); } } catch (err, stack) { Log.d('_launchDeployKeyPage: ' + err.toString()); Log.d(stack.toString()); } } void _launchCreateRepoPage() async { assert(_gitHostType != GitHostType.Unknown); try { if (_gitHostType == GitHostType.GitHub) { await launch("https://github.com/new"); } else if (_gitHostType == GitHostType.GitLab) { await launch("https://gitlab.com/projects/new"); } } catch (err, stack) { // FIXME: Error handling? Log.d("_launchCreateRepoPage: " + err.toString()); Log.d(stack.toString()); } } void _startGitClone(BuildContext context) async { setState(() { gitCloneErrorMessage = ""; }); var stateContainer = Provider.of(context); var appState = stateContainer.appState; var basePath = appState.gitBaseDirectory; // Just in case it was half cloned because of an error String repoPath = p.join(basePath, widget.repoFolderName); await _removeExistingClone(repoPath); String error; try { Log.d("Cloning " + _gitCloneUrl); await GitRepo.clone(repoPath, _gitCloneUrl); } on GitException catch (e) { Log.e(e.toString()); error = e.cause; } if (error != null && error.isNotEmpty) { setState(() { logEvent(Event.GitHostSetupGitCloneError, parameters: { 'error': error, }); gitCloneErrorMessage = error; }); return; } // // Add a GitIgnore file. This way we always at least have one commit // It makes doing a git pull and push easier // var anyFileInRepo = Directory(repoPath).list().firstWhere( (fs) => fs.statSync().type == FileSystemEntityType.file, orElse: () => null, ); if (anyFileInRepo == null) { Log.i("Adding .ignore file"); var ignoreFile = File(p.join(repoPath, ".gitignore")); ignoreFile.createSync(); var repo = GitRepo( folderPath: repoPath, ); await repo.add('.gitignore'); var settings = Provider.of(context, listen: false); await repo.commit( message: "Add gitignore file", authorEmail: settings.gitAuthorEmail, authorName: settings.gitAuthor, ); } logEvent( Event.GitHostSetupComplete, parameters: _buildOnboardingAnalytics(), ); Navigator.pop(context); widget.onCompletedFunction(widget.repoFolderName); } Future _completeAutoConfigure() async { Log.d("Starting autoconfigure copletion"); try { Log.i("Generating SSH Key"); setState(() { _autoConfigureMessage = "Generating SSH Key"; }); var publicKey = await generateSSHKeys(comment: "GitJournal"); Log.i("Adding as a deploy key"); _autoConfigureMessage = "Adding as a Deploy Key"; await _gitHost.addDeployKey(publicKey, _gitHostRepo.fullName); } on Exception catch (e, stacktrace) { _handleGitHostException(e, stacktrace); return; } setState(() { _gitCloneUrl = _gitHostRepo.cloneUrl; _pageCount += 1; _nextPage(); _startGitClone(context); }); } void _handleGitHostException(Exception e, StackTrace stacktrace) { Log.d("GitHostSetupAutoConfigureComplete: " + e.toString()); setState(() { _autoConfigureErrorMessage = e.toString(); logEvent( Event.GitHostSetupError, parameters: { 'errorMessage': _autoConfigureErrorMessage, }, ); logException(e, stacktrace); }); } Map _buildOnboardingAnalytics() { var map = {}; if (_gitCloneUrl.contains("github.com")) { map["host_type"] = "GitHub"; } else if (_gitCloneUrl.contains("gitlab.org")) { map["host_type"] = "GitLab.org"; } else if (_gitCloneUrl.contains("gitlab")) { map["host_type"] = "GitLab"; } var ch0 = _pageChoice[0] as PageChoice0; map["provider_choice"] = ch0.toString().replaceFirst("PageChoice0.", ""); var ch1 = _pageChoice[1] as PageChoice1; map["setup_manner"] = ch1.toString().replaceFirst("PageChoice1.", ""); map["key_generation"] = _keyGenerationChoice .toString() .replaceFirst("KeyGenerationChoice.", ""); return map; } Future _removeExistingClone(String baseDirPath) async { var baseDir = Directory(baseDirPath); var dotGitDir = Directory(p.join(baseDir.path, ".git")); bool exists = dotGitDir.existsSync(); if (exists) { Log.d("Removing " + baseDir.path); await baseDir.delete(recursive: true); await baseDir.create(); } } } class GitHostChoicePage extends StatelessWidget { final Func1 onKnownGitHost; final Func0 onCustomGitHost; GitHostChoicePage({ @required this.onKnownGitHost, @required this.onCustomGitHost, }); @override Widget build(BuildContext context) { return Container( child: Column( children: [ Text( "Select a Git Hosting Provider -", style: Theme.of(context).textTheme.headline5, ), const SizedBox(height: 16.0), GitHostSetupButton( text: "GitHub", iconUrl: 'assets/icon/github-icon.png', onPressed: () { onKnownGitHost(GitHostType.GitHub); }, ), const SizedBox(height: 8.0), GitHostSetupButton( text: "GitLab", iconUrl: 'assets/icon/gitlab-icon.png', onPressed: () async { onKnownGitHost(GitHostType.GitLab); }, ), const SizedBox(height: 8.0), GitHostSetupButton( text: "Custom", onPressed: () async { onCustomGitHost(); }, ), ], mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, ), ); } } enum GitHostSetupType { Auto, Manual, } class GitHostAutoConfigureChoicePage extends StatelessWidget { final Func1 onDone; GitHostAutoConfigureChoicePage({@required this.onDone}); @override Widget build(BuildContext context) { return Container( child: Column( children: [ Text( "How do you want to do this?", style: Theme.of(context).textTheme.headline5, ), const SizedBox(height: 16.0), GitHostSetupButton( text: "Setup Automatically", onPressed: () { onDone(GitHostSetupType.Auto); }, ), const SizedBox(height: 8.0), GitHostSetupButton( text: "Let me do it manually", onPressed: () async { onDone(GitHostSetupType.Manual); }, ), ], mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, ), ); } }