├── assets ├── pull.html ├── style.css ├── fork.html ├── clone.html ├── commit.html ├── settings.html ├── branch.html └── create.html ├── README.md └── GHForPG.js /assets/pull.html: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Installation 2 | 1) Open the desktop version of Pinegrow - this plugin **will not** work with Pinegrow Live 3 | 2) Select File -> Manage libraries & plugins... 4 | 3) Click on "Load plugin" at the bottom 5 | 4) Navigate to the plugin folder and select "GHForPG.js" 6 | 5) I recommend restarting Pinegrow, but this isn't neccessary. 7 | 8 | ## Usage 9 | This plugin will add a new "GitHub" menu to Pinegrow. There are five selections when a project is not open and two additional items when one is open. 10 | 11 | ### Settings 12 | The setting selection opens a modal to add in your user name, email, and personal token for GitHub. These will be stored in a localStorage variable in Pinegrow and should be retained between restarts. There is a "Retrieve Settings" button that will get the user name and email for a project, but not the authorization token. All further actions by the plugin require authorization. 13 | 14 | ## New Repo Controls 15 | ### Create New Repo 16 | This will allow the creation of a new GitHub repo, either from scratch or from an existing project. If the project doesn't exist, a base folder will be created that should then be opened as a Pinegrow project. 17 | 18 | ### Clone Existing Repo 19 | This will create a local copy of a repo from GitHub and link this local copy to the repo on GitHub 20 | 21 | ### Branch Existing Repo 22 | This will create a local copy and a new branch of an existing GitHub repo. 23 | 24 | ### Fork Existing Repo 25 | This will create a local copy and a duplicate of a repo on GitHub from a foreign account into the user's account. 26 | 27 | ## Existing Repo Controls 28 | ### Stage, Commit, and Push Changes 29 | This allows you to select which files are committed to a repo. It flags changed and added files. 30 | 1) Select files and click "Stage files" 31 | 32 | -If you make a mistake you can select a file and click "Unstage files" 33 | 34 | 2) Enter the author email 35 | 3) Enter a commit message 36 | 4) Click on "Commit files" 37 | 5) Click on "Push files" 38 | 39 | ### Pull changes 40 | This will retrieve any changes made to the GitHub repo that aren't present in the local repo. -------------------------------------------------------------------------------- /assets/style.css: -------------------------------------------------------------------------------- 1 | /*From create.html*/ 2 | .gh-error { 3 | color: red; 4 | display: none; 5 | } 6 | 7 | .gh-logged-in { 8 | color: green; 9 | display: none; 10 | } 11 | 12 | .gh-message-area { 13 | width: 100%; 14 | height: 6em; 15 | display: flex; 16 | justify-content: center; 17 | } 18 | 19 | .gh-message-box { 20 | width: 100%; 21 | height: 6em; 22 | border: 1px solid #ccc; 23 | border-radius: 4px; 24 | padding-left: 12px; 25 | padding-right: 12px; 26 | overflow-y: scroll; 27 | } 28 | 29 | /* From clone.html */ 30 | #gh-clone-verification { 31 | visibility: hidden; 32 | margin-left: 20px; 33 | } 34 | 35 | #gh-clone-verification.visible { 36 | visibility: visible; 37 | } 38 | 39 | /* From branch.html */ 40 | #gh-branch-verification { 41 | display: none; 42 | margin-left: 20px; 43 | } 44 | 45 | #gh-branch-verification.visible { 46 | visibility: visible; 47 | } 48 | 49 | /* From commit.html */ 50 | #ghProjectTree { 51 | list-style: none; 52 | padding: 0; 53 | } 54 | 55 | #ghProjectTree input { 56 | margin-right: 10px; 57 | } 58 | 59 | #ghProjectTree ul { 60 | list-style: none; 61 | } 62 | 63 | .gh-commit-error { 64 | color: red; 65 | display: none; 66 | } 67 | 68 | .gh-visible { 69 | display: block; 70 | } 71 | 72 | .gh-hidden { 73 | display: none; 74 | } 75 | 76 | #gh-commit-modal i.folder-icon { 77 | color: black; 78 | } 79 | 80 | #gh-commit-modal li.file { 81 | padding-left: 1em; 82 | } 83 | 84 | .gh-disabled { 85 | color: #ccc; 86 | } 87 | 88 | .gh-ml { 89 | margin-left: 20px; 90 | } 91 | 92 | .gh-mt { 93 | margin-top: 20px; 94 | } 95 | 96 | [data-gh-status]::before{ 97 | content: "*"; 98 | color: white; 99 | } 100 | 101 | [data-gh-status="*modified"]::before { 102 | color: red; 103 | } 104 | 105 | [data-gh-status="*added"]::before { 106 | color: red; 107 | } 108 | 109 | [data-gh-status="modified"]::before { 110 | color: blue; 111 | } 112 | 113 | [data-gh-status="added"]::before { 114 | color: blue; 115 | } 116 | 117 | .gh-red { 118 | color: red; 119 | } 120 | 121 | .gh-blue { 122 | color: blue; 123 | } 124 | 125 | 126 | 127 | /* From fork.html */ 128 | #gh-fork-verification { 129 | visibility: hidden; 130 | margin-left: 20px; 131 | } 132 | -------------------------------------------------------------------------------- /assets/fork.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/clone.html: -------------------------------------------------------------------------------- 1 | 56 | -------------------------------------------------------------------------------- /assets/commit.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/settings.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/branch.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/create.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /GHForPG.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | $('body').one('pinegrow-ready', function (e, pinegrow) { 3 | 4 | //Add a framework id, it should be unique to this framework and version. Best practice is to define the prefix as a variable that can be used throughout the framework. 5 | let framework_id = 'shd_github_for_pinegrow'; 6 | 7 | //Instantiate a new framework 8 | var framework = new PgFramework(framework_id, 'GitHub-for-Pinegrow'); 9 | 10 | // Define a framework type - if you plan on having multiple versions, this should be the same for each version. 11 | framework.type = 'GitHub-for-Pinegrow'; 12 | 13 | //Prevent the activation of multiple versions of the framework - if this should be allowed, change to false 14 | framework.allow_single_type = true; 15 | 16 | //Optional, add a badge to the framework list notify user of new or updated status 17 | //framework.info_badge = 'v1.0.0'; 18 | 19 | //Add a description of the plugin 20 | framework.description = 'Adds GitHub functionality to Pinegrow'; 21 | 22 | //Add a framework author to be displayed with the framework templates 23 | framework.author = 'Robert "Bo" Means'; 24 | 25 | //Add a website "https://pinegrow.com" or mailto "mailto:info@pinegrow.com" link for redirect on author name click 26 | framework.author_link = 'https://robertmeans.net'; 27 | 28 | // Tell Pinegrow about the framework 29 | pinegrow.addFramework(framework); 30 | 31 | //uncomment the line below for debugging - opens devtools on Pinegrow Launch 32 | //require('nw.gui').Window.get().showDevTools(); 33 | 34 | //Establish the base directory for node modules 35 | const frameBase = framework.getBaseUrl(); 36 | 37 | //Load in the Octokit module 38 | const { 39 | Octokit 40 | } = require(crsaMakeFileFromUrl(frameBase + '/node_modules/@octokit/rest/dist-node/index.js')); 41 | 42 | //Load in Git functions from the isomorphic-git library 43 | const Git = require(crsaMakeFileFromUrl(frameBase + '/node_modules/isomorphic-git/index.cjs')); 44 | const http = require(crsaMakeFileFromUrl(frameBase + '/node_modules/isomorphic-git/http/node/index.cjs')); 45 | 46 | //Load in glob for easier file handling 47 | const glob = require(crsaMakeFileFromUrl(frameBase + '/node_modules/fast-glob/out/index.js')); 48 | 49 | //Add in better folder selector 50 | const openFolderExplorer = require(crsaMakeFileFromUrl(frameBase + '/node_modules/nw-programmatic-folder-select/index.js')); 51 | 52 | //load in file management packages 53 | const Path = require('path'); 54 | const fse = require('fs-extra'); 55 | 56 | //Function to poulate settings with existing values, clear settings, save new settings. 57 | let ghManipulateSettingsFields = () => { 58 | //First, let's get all of the form fields and buttons 59 | let userNameField = ghById('gh-user-name'); 60 | let emailField = ghById('gh-user-email'); 61 | let accountTokenField = ghById('gh-token'); 62 | let retrieveSettingsButton = ghById('gh-retrieve-settings'); 63 | let clearSettingsButton = ghById('gh-clear-settings'); 64 | let saveSettingsButton = ghById('gh-save-settings'); 65 | let cancelSettingsButton = ghById('gh-cancel-settings'); 66 | let userErrorMessage = ghById('gh-username-error'); 67 | let emailErrorMessage = ghById('gh-email-error'); 68 | let tokenErrorMessage = ghById('gh-token-error'); 69 | let credentialsErrorMessage = ghById('gh-git-config-missing'); 70 | let projectNotOpenError = ghById('gh-settings-project-error'); 71 | let credentialsNotificationMessage = ghById('gh-settings-retrieved-notification'); 72 | let credentialsMessage = ghById('gh-correct-credentials'); 73 | let configNotification = ghById('gh-settings-config-notification'); 74 | let showToken = ghById('gh-show-token'); 75 | //check if the user has already stored any credentials in local storage, if so add them to the form 76 | if (localStorage.getItem('gh-settings-user-name')) { 77 | userNameField.value = localStorage.getItem('gh-settings-user-name'); 78 | emailField.value = localStorage.getItem('gh-settings-email'); 79 | accountTokenField.value = localStorage.getItem('gh-settings-token'); 80 | } 81 | 82 | //Allow toggle of the token field between plain text and encoded 83 | showToken.addEventListener('click', () => { 84 | accountTokenField.type = showToken.checked ? "text" : "password"; 85 | }); 86 | 87 | //allow user to retrieve settings from gitconfig file 88 | retrieveSettingsButton.addEventListener('click', async () => { 89 | //Throw error if project isn't open 90 | if (!pinegrow.getCurrentProject()) { 91 | projectNotOpenError.style.visibility = 'visible'; 92 | return; 93 | } 94 | let projectDirectory = ghProjectDirectory(); 95 | //Get gitconfig file 96 | let ghCredentials = await ghFetchGitConfig(projectDirectory); 97 | //Throw error if file doesn't exist 98 | if (ghCredentials === false || ghCredentials === undefined) { 99 | credentialsErrorMessage.style.visibility = 'visible'; 100 | return; 101 | } 102 | if (ghCredentials.userName === undefined || ghCredentials.userToken === undefined) { 103 | credentialsErrorMessage.style.visibility = 'visible'; 104 | localStorage.setItem('gh-config-file-error', true); 105 | return; 106 | } 107 | //Set form values to retrieved credentials 108 | credentialsNotificationMessage.style.visibility = 'visible'; 109 | userNameField.value = ghCredentials.userName; 110 | accountTokenField.value = ghCredentials.userToken; 111 | }); 112 | 113 | clearSettingsButton.addEventListener('click', () => { 114 | localStorage.removeItem('gh-settings-user-name'); 115 | localStorage.removeItem('gh-settings-email'); 116 | localStorage.removeItem('gh-settings-token'); 117 | localStorage.removeItem('gh-config-file-error'); 118 | userNameField.value = ''; 119 | emailField.value = ''; 120 | accountTokenField.value = ''; 121 | accountTokenField.type = "password"; 122 | showToken.checked = false; 123 | saveSettingsButton.className = 'btn btn-primary'; 124 | userErrorMessage.style.visibility = 'hidden'; 125 | emailErrorMessage.style.visibility = 'hidden'; 126 | tokenErrorMessage.style.visibility = 'hidden'; 127 | credentialsErrorMessage.style.visibility = 'hidden'; 128 | projectNotOpenError.style.visibility = 'hidden'; 129 | credentialsNotificationMessage.style.visibility = 'hidden'; 130 | credentialsMessage.style.visibility = 'hidden'; 131 | if (pinegrow.getCurrentProject()) { 132 | let projectDirectory = ghProjectDirectory(); 133 | ghDeleteGitConfig(projectDirectory); 134 | } 135 | }); 136 | 137 | saveSettingsButton.addEventListener('click', async () => { 138 | localStorage.setItem('gh-settings-user-name', userNameField.value); 139 | localStorage.setItem('gh-settings-email', emailField.value); 140 | localStorage.setItem('gh-settings-token', accountTokenField.value); 141 | if (emailField.value === '' || emailField.value === null) { 142 | emailErrorMessage.style.visibility = 'visible'; 143 | return; 144 | } 145 | let errorCheck = await ghVerifyGitHubAccount(); 146 | if (true === errorCheck) { 147 | saveSettingsButton.className = 'btn btn-success'; 148 | userErrorMessage.style.visibility = 'hidden'; 149 | emailErrorMessage.style.visibility = 'hidden'; 150 | tokenErrorMessage.style.visibility = 'hidden'; 151 | credentialsErrorMessage.style.visibility = 'hidden'; 152 | projectNotOpenError.style.visibility = 'hidden'; 153 | credentialsNotificationMessage.style.visibility = 'hidden'; 154 | credentialsMessage.style.visibility = 'visible'; 155 | accountTokenField.type = "password"; 156 | if (pinegrow.getCurrentProject()) { 157 | let projectDirectory = ghProjectDirectory(); 158 | if (localStorage.getItem('gh-config-file-error')) { 159 | configNotification.style.visibility = 'visible'; 160 | localStorage.removeItem('gh-config-file-error'); 161 | ghWriteGitConfig(projectDirectory); 162 | } 163 | } 164 | } else if (-1 === errorCheck) { 165 | userErrorMessage.style.visibility = 'visible'; 166 | localStorage.removeItem('gh-settings-user-name'); 167 | credentialsMessage.style.visibility = 'hidden'; 168 | emailErrorMessage.style.visibility = 'hidden'; 169 | tokenErrorMessage.style.visibility = 'hidden'; 170 | credentialsNotificationMessage.style.visibility = 'hidden'; 171 | } else { 172 | tokenErrorMessage.style.visibility = 'visible'; 173 | localStorage.removeItem('gh-settings-token'); 174 | credentialsMessage.style.visibility = 'hidden'; 175 | userErrorMessage.style.visibility = 'hidden'; 176 | emailErrorMessage.style.visibility = 'hidden'; 177 | credentialsNotificationMessage.style.visibility = 'hidden'; 178 | } 179 | }); 180 | 181 | cancelSettingsButton.addEventListener('click', () => { 182 | saveSettingsButton.className = 'btn btn-primary'; 183 | userErrorMessage.style.visibility = 'hidden'; 184 | tokenErrorMessage.style.visibility = 'hidden'; 185 | credentialsMessage.style.visibility = 'hidden'; 186 | projectNotOpenError.style.visibility = 'hidden'; 187 | credentialsNotificationMessage.style.visibility = 'hidden' 188 | accountTokenField.type = "password"; 189 | showToken.checked = false; 190 | }) 191 | }; 192 | 193 | //Function to gather new repo information. 194 | let ghManipulateCreateFields = async () => { 195 | let createModal = ghById('gh-create-modal'); 196 | let closeButton = ghById('gh-create-close-button'); 197 | let repoNameField = ghById('gh-new-repo-name'); 198 | let useExistingControl = ghById('gh-use-existing-folder'); 199 | let repoDescription = ghById('gh-repo-description'); 200 | let repoCommitMessage = ghById('gh-create-commit-message'); 201 | let repoCommitMessageGroup = ghById('gh-create-commit-message-group'); 202 | let repoLicense = ghById('gh-repo-license'); 203 | let repoPrivate = ghById('gh-repo-private'); 204 | let repoInitialize = ghById('gh-auto-init'); 205 | let repoInitializeGroup = ghById('gh-auto-initialize-group'); 206 | let repoNameError = ghById('gh-repo-name-error'); 207 | let newFolderGroup = ghById('gh-create-new-folder-group'); 208 | let folderButton = ghById('gh-get-new-save-folder'); 209 | let folderPath = ghById('gh-new-save-location'); 210 | let folderNameGroup = ghById('gh-new-folder-name-group'); 211 | let folderName = ghById('gh-new-folder-name'); 212 | let useExisting = false; 213 | let correctCredentials = ghById('gh-create-correct-credentials'); 214 | let incorrectCredentials = ghById('gh-create-incorrect-credentials'); 215 | let createMessage = ghById('gh-create-message-box'); 216 | let createNewRepo = ghById('gh-create-repo-button'); 217 | let createResetButton = ghById('gh-create-reset'); 218 | let createPathError = ghById('gh-create-path-error'); 219 | let createLicenseError = ghById('gh-license-error'); 220 | let newFolderNameError = ghById('gh-create-folder-name-error'); 221 | 222 | createMessage.innerHTML = 'No message at this time
'; 223 | 224 | useExistingControl.addEventListener('click', () => { 225 | if (pinegrow.getCurrentProject()) { 226 | if (useExistingControl.checked) { 227 | newFolderGroup.style.display = 'none'; 228 | folderNameGroup.style.display = 'none'; 229 | repoCommitMessageGroup.style.display = 'block'; 230 | repoInitializeGroup.style.display = 'none'; 231 | useExisting = true; 232 | let currentProject = pinegrow.getCurrentProject(); 233 | let sanitizedName = ghSanitizeRepoName(currentProject.name) 234 | repoNameField.value = sanitizedName; 235 | } else { 236 | newFolderGroup.style.display = 'block'; 237 | folderNameGroup.style.display = 'block'; 238 | repoCommitMessageGroup.style.display = 'none'; 239 | repoInitializeGroup.style.display = 'block'; 240 | useExisting = false; 241 | repoNameField.value = ''; 242 | } 243 | } else { 244 | createMessage.innerHTML = 'You must have a project open to use this option.
'; 245 | useExistingControl.checked = false; 246 | } 247 | }); 248 | 249 | repoNameField.addEventListener('input', () => { 250 | if (!useExistingControl.checked) { 251 | folderName.value = repoNameField.value; 252 | } 253 | }); 254 | 255 | folderButton.addEventListener('click', () => { 256 | openFolderExplorer(window, (selection) => { 257 | folderPath.innerHTML = selection; 258 | }); 259 | }); 260 | 261 | createResetButton.addEventListener('click', () => { 262 | folderPath.innerHTML = ''; 263 | createModal.querySelectorAll('.gh-error').forEach(el => { 264 | el.style.display = 'none'; 265 | }); 266 | createModal.querySelector('.gh-logged-in').style.display = 'none'; 267 | createMessage.innerHTML = 'No message at this time
'; 268 | createNewRepo.disabled = false; 269 | }) 270 | 271 | createNewRepo.addEventListener('click', async () => { 272 | let errorCheck = await ghVerifyGitHubAccount(); 273 | if (!errorCheck) { 274 | createMessage.innerHTML = 'Please check your credentials.
'; 275 | incorrectCredentials.style.display = "block"; 276 | return; 277 | } else { 278 | correctCredentials.style.display = "block"; 279 | } 280 | if (repoNameField.value === '' || repoNameField.value === undefined) { 281 | createMessage.innerHTML = 'Please add a repo name.
'; 282 | repoNameError.style.display = 'block'; 283 | return; 284 | } 285 | if (repoLicense.value === '') { 286 | createMessage.innerHTML = 'Please select a license.
'; 287 | createLicenseError.style.display = 'block'; 288 | return; 289 | } 290 | if (folderPath.innerHTML === '' && !useExisting) { 291 | createMessage.innerHTML = 'Please add a path to save your local repo.
'; 292 | createPathError.style.display = 'block'; 293 | return; 294 | } 295 | if ((folderName.value === '' || folderName.value === undefined) && !useExisting) { 296 | createMessage.innerHTML = 'Please add a name for your local repo folder.
'; 297 | newFolderNameError.style.display = 'block'; 298 | return; 299 | } 300 | createMessage.innerHTML = ""; 301 | let repoArgs = { 302 | "name": repoNameField.value 303 | }; 304 | repoArgs['description'] = repoDescription.value; 305 | repoArgs['license_template'] = repoLicense.value; 306 | 307 | if (repoPrivate.checked) { 308 | repoArgs['private'] = true; 309 | } 310 | 311 | if (repoInitialize.checked) { 312 | repoArgs['auto_init'] = true; 313 | } 314 | 315 | let repoCreated = await ghCreateRepo(repoArgs); 316 | ghStatusUpdate(repoCreated.status, createMessage, 'New Repo created.'); 317 | 318 | if (!useExisting && repoCreated.status == "201") { 319 | let owner = localStorage.getItem('gh-settings-user-name'); 320 | let newFolder = folderName.value; 321 | let dir = Path.join(folderPath.innerHTML, newFolder); 322 | let repo = "https://github.com/" + owner + "/" + repoNameField.value; 323 | const onAuth = () => ({ 324 | username: localStorage.getItem('gh-settings-user-name'), 325 | password: localStorage.getItem('gh-settings-token') 326 | }); 327 | await ghClone(onAuth, repo, dir, createMessage, ''); 328 | let content = '_pgbackup'; 329 | let ignoreFile = Path.join(dir, '.gitignore'); 330 | try { 331 | fse.writeFileSync(ignoreFile, content); 332 | } catch (err) { 333 | console.error(err); 334 | } 335 | let projectData = { 336 | repo: repoNameField.value, 337 | owner: owner, 338 | branch: 'main' 339 | }; 340 | ghCreateJsonFile(projectData, dir, 'githubinfo.json'); 341 | ghWriteGitConfig(dir); 342 | } else if (useExisting && repoCreated.status == "201") { 343 | let owner = localStorage.getItem('gh-settings-user-name'); 344 | 345 | let projectDirectory = ghProjectDirectory(); 346 | await Git.init({ 347 | fs: fse, 348 | dir: projectDirectory, 349 | defaultBranch: 'main' 350 | }); 351 | let projectData = { 352 | repo: repoNameField.value, 353 | owner: owner, 354 | branch: 'main' 355 | }; 356 | let commitMessage = (repoCommitMessage.value) ? repoCommitMessage.value : undefined; 357 | ghCreateJsonFile(projectData, projectDirectory, 'githubinfo.json'); 358 | let remoteUrl = 'https://github.com/' + owner + '/' + repoNameField.value; 359 | let newAccountConfigValues = `[remote "origin"] 360 | fetch = +refs/heads/*:refs/remotes/origin/* 361 | url = ${remoteUrl} 362 | [branch "main"] 363 | merge = refs/heads/main 364 | remote = origin` 365 | await ghWriteConfig(newAccountConfigValues, projectDirectory); 366 | await ghWriteGitConfig(projectDirectory); 367 | let uploadStatus = await ghUploadToRepoNew(projectDirectory, projectData, 'main', commitMessage); 368 | ghStatusUpdate(uploadStatus, createMessage, 'Files uploaded'); 369 | } 370 | createNewRepo.disabled = true; 371 | }); 372 | 373 | closeButton.addEventListener('click', () => { 374 | createResetButton.click(); 375 | }) 376 | }; 377 | 378 | //Function to gather existing repo information. 379 | let ghManipulateCloneFields = async () => { 380 | let repoOwner = ghById('gh-existing-repo-owner'); 381 | let repoName = ghById('gh-existing-repo-name'); 382 | let branchName = ghById('gh-clone-branch-name'); 383 | let folderButton = ghById('gh-get-save-folder'); 384 | let saveLocation = ghById('gh-save-location'); 385 | let folderName = ghById('gh-clone-folder-name'); 386 | let folderVerification = ghById('gh-clone-verification'); 387 | let folderOkayButton = ghById('gh-clone-folder-okay'); 388 | let folderCancelButton = ghById('gh-clone-folder-cancel'); 389 | let clearButton = ghById('gh-clone-clear'); 390 | let cloneButton = ghById('gh-get-existing-repo-button'); 391 | let loginError = ghById('gh-clone-log-error'); 392 | let logCheck = await ghVerifyGitHubAccount(); 393 | if (logCheck !== true) { 394 | loginError.style.visibility = 'visible'; 395 | } else { 396 | loginError.style.visibility = 'hidden'; 397 | } 398 | let cloneMessage = ghById('gh-clone-message-box'); 399 | cloneMessage.innerHTML = 'No message at this time
'; 400 | folderButton.addEventListener('click', () => { 401 | openFolderExplorer(window, (selection) => { 402 | saveLocation.innerHTML = selection; 403 | }); 404 | }); 405 | 406 | //Redo error checking? 407 | cloneButton.addEventListener('click', async () => { 408 | if (logCheck === true && folderName.value && saveLocation.innerHTML != '' && repoOwner.value && repoName.value) { 409 | let cloneBranch = (branchName.value) ? branchName.value : ''; 410 | cloneMessage.innerHTML = ""; 411 | let newFolder = folderName.value; 412 | let dir = Path.join(saveLocation.innerHTML, newFolder); 413 | let dirCheck = fse.existsSync(dir); 414 | if (dirCheck) { 415 | folderVerification.classList.add('visible'); 416 | cloneMessage.innerHTML = "Please verify existing folder overwrite.
"; 417 | } else { 418 | let url = "https://github.com/" + repoOwner.value + "/" + repoName.value + '.git'; 419 | const onAuth = () => ({ 420 | username: localStorage.getItem('gh-settings-user-name'), 421 | password: localStorage.getItem('gh-settings-token') 422 | }); 423 | let cloneRepo = await ghClone(onAuth, url, dir, cloneMessage, cloneBranch); 424 | if (cloneRepo) { 425 | let successMessage = document.createElement('p'); 426 | successMessage.innerHTML = 'Repo successfully cloned.'; 427 | cloneMessage.appendChild(successMessage); 428 | let projectData = { 429 | repo: repoName.value, 430 | owner: owner, 431 | branch: 'main' 432 | }; 433 | ghCreateJsonFile(projectData, dir, 'githubinfo.json'); 434 | ghWriteGitConfig(dir); 435 | let content = '_pgbackup'; 436 | let ignoreFile = Path.join(dir, '.gitignore'); 437 | try { 438 | const gitIgnore = fse.writeFileSync(ignoreFile, content); 439 | } catch (err) { 440 | console.error(err); 441 | } 442 | } 443 | } 444 | } else { 445 | cloneMessage.innerHTML = 'Check that your credentials are valid and you have filled out all of the fields.
'; 446 | } 447 | }); 448 | 449 | folderOkayButton.addEventListener('click', async () => { 450 | cloneMessage.innerHTML = ''; 451 | let newFolder = folderName.value 452 | let dir = Path.join(saveLocation.innerHTML, newFolder); 453 | let cloneBranch = (branchName.value) ? branchName.value : ''; 454 | let url = "https://github.com/" + repoOwner.value + "/" + repoName.value + '.git'; 455 | const onAuth = () => ({ 456 | username: localStorage.getItem('gh-settings-user-name'), 457 | password: localStorage.getItem('gh-settings-token') 458 | }); 459 | await ghClone(onAuth, url, dir, cloneMessage, cloneBranch); 460 | if (cloneRepo) { 461 | let successMessage = document.createElement('p'); 462 | successMessage.innerHTML = 'Repo successfully cloned.'; 463 | cloneMessage.appendChild(successMessage); 464 | let projectData = { 465 | repo: repoName.value, 466 | owner: owner, 467 | branch: 'main' 468 | }; 469 | ghCreateJsonFile(projectData, dir, 'githubinfo.json'); 470 | ghWriteGitConfig(dir); 471 | let ignoreFile = Path.join(dir, '.gitignore'); 472 | try { 473 | const gitIgnore = fse.writeFileSync(ignoreFile, content); 474 | } catch (err) { 475 | console.error(err); 476 | } 477 | } 478 | }); 479 | 480 | folderCancelButton.addEventListener('click', () => { 481 | folderVerification.classList.remove('visible'); 482 | cloneMessage.innerHTML = ""; 483 | }); 484 | 485 | clearButton.addEventListener('click', () => { 486 | repoOwner.value = ''; 487 | repoName.value = ''; 488 | saveLocation.innerHTML = ''; 489 | folderName.value = ''; 490 | cloneMessage.innerHTML = ''; 491 | if (folderVerification.classList.contains('visible')) folderVerification.classList.remove('visible') 492 | }); 493 | }; 494 | 495 | //Function to gather branch info 496 | let ghManipulateBranchFields = async () => { 497 | let branchRepoOwner = ghById('gh-branch-repo-owner'); 498 | let branchRepoName = ghById('gh-branch-repo-name'); 499 | let branchBranchName = ghById('gh-branch-branch-name'); 500 | let branchNewName = ghById('gh-branch-new-name'); 501 | let branchFolderButton = ghById('gh-branch-get-save-folder'); 502 | let branchFolderPath = ghById('gh-branch-save-location'); 503 | let branchFolderName = ghById('gh-branch-folder-name'); 504 | let branchVerification = ghById('gh-branch-verification'); 505 | let branchOkay = ghById('gh-branch-folder-okay'); 506 | let branchCancel = ghById('gh-branch-folder-cancel'); 507 | let branchMessage = ghById('gh-branch-message-box'); 508 | let branchSubmit = ghById('gh-branch-submit'); 509 | let branchReset = ghById('gh-branch-reset-button'); 510 | let branchClose = ghById('gh-branch-close'); 511 | let correctCredentials = ghById('gh-branch-correct-credentials'); 512 | let incorrectCredentials = ghById('gh-branch-incorrect-credentials'); 513 | 514 | branchMessage.innerHTML = 'No message at this time.
'; 515 | 516 | let errorCheck = await ghVerifyGitHubAccount(); 517 | if (errorCheck) { 518 | correctCredentials.style.display = 'block'; 519 | incorrectCredentials.style.display = 'none'; 520 | } else { 521 | correctCredentials.style.display = 'none'; 522 | incorrectCredentials.style.display = 'block'; 523 | } 524 | 525 | let octokit = await ghCreateOctokitInstance(); 526 | 527 | branchNewName.addEventListener('input', () => branchFolderName.value = branchNewName.value); 528 | 529 | branchFolderButton.addEventListener('click', () => { 530 | openFolderExplorer(window, (selection) => { 531 | branchFolderPath.innerHTML = selection; 532 | }); 533 | }); 534 | 535 | branchReset.addEventListener('click', () => { 536 | branchFolderPath.innerHTML = ''; 537 | branchMessage.innerHTML = ''; 538 | if (branchVerification.classList.contains('visible')) branchVerification.classList.remove('visible'); 539 | }); 540 | 541 | branchClose.addEventListener('click', () => branchReset.click()); 542 | 543 | branchSubmit.addEventListener('click', async () => { 544 | if (errorCheck && branchRepoOwner.value && branchRepoName.value && branchNewName.value && (branchFolderPath.innerHTML != '' || branchFolderPath.innerHTML != undefined) && branchFolderName.value) { 545 | let branchBranch = (branchBranchName.value) ? branchBranchName.value : ''; 546 | branchMessage.innerHTML = ''; 547 | let newFolder = branchFolderName.value; 548 | let dir = Path.join(branchFolderPath.innerHTML, newFolder); 549 | let dirCheck = fse.existsSync(dir); 550 | if (dirCheck) { 551 | branchVerification.classList.add('visible'); 552 | branchMessage.innerHTML('Please verify existing folder overwrite.
'); 553 | } else { 554 | let cloneAndBranch = await ghCloneAndBranch(dir, branchMessage, branchBranch); 555 | } 556 | } else { 557 | cloneMessage.innerHTML = 'Check that your credentials are valid and you have filled out all of the fields.
'; 558 | } 559 | }); 560 | 561 | branchOkay.addEventListener('click', async () => { 562 | let branchBranch = (branchBranchName.value) ? branchBranchName.value : ''; 563 | branchMessage.innerHTML = ''; 564 | let newFolder = branchFolderName.value; 565 | let dir = Path.join(branchFolderPath.innerHTML, newFolder); 566 | let cloneAndBranch = await ghCloneAndBranch(dir, branchMessage, branchBranch); 567 | }); 568 | 569 | branchCancel.addEventListener('click', function () { 570 | branchVerification.classList.remove('visible'); 571 | branchMessage.innerHTML = ""; 572 | }); 573 | 574 | let ghCloneAndBranch = async (dir, branchMessage, branchBranch) => { 575 | let url = "https://github.com/" + branchRepoOwner.value + "/" + branchRepoName.value + '.git'; 576 | const onAuth = () => ({ 577 | username: localStorage.getItem('gh-settings-user-name'), 578 | password: localStorage.getItem('gh-settings-token') 579 | }); 580 | let baseRepoCreate = await ghClone(onAuth, url, dir, branchMessage, branchBranch); 581 | if (baseRepoCreate) { 582 | let successMessage = document.createElement('p'); 583 | successMessage.innerHTML = 'Repo successfully cloned.'; 584 | branchMessage.appendChild(successMessage); 585 | let currentBranch = (branchBranchName.value === '' || branchBranchName.value === undefined) ? 'main' : branchBranchName.value; 586 | let args = { 587 | octokit: octokit, 588 | owner: branchRepoOwner.value, 589 | repo: branchRepoName.value, 590 | currentBranch, 591 | newBranch: branchNewName.value 592 | } 593 | let currentSha = await ghGetCurrentCommit(args); 594 | 595 | let createBranch = await ghCreateRef(args, currentSha.commitSha); 596 | branchMessage.innerHTML = 'Branch created and reference updated.
'; 597 | let projectData = { 598 | repo: args.repo, 599 | owner: args.owner, 600 | branch: args.newBranch 601 | } 602 | ghCreateJsonFile(projectData, dir, 'githubinfo.json'); 603 | ghWriteGitConfig(dir); 604 | } 605 | } 606 | } 607 | 608 | let ghManipulateForkFields = async () => { 609 | let closeButton = ghById('gh-fork-close'); 610 | let forkOwner = ghById('gh-fork-repo-owner'); 611 | let forkNameField = ghById('gh-fork-repo-name'); 612 | let forkSaveFolderButton = ghById('gh-fork-get-save-folder'); 613 | let forkSaveLocation = ghById('gh-fork-save-location'); 614 | let forkNewFolderName = ghById('gh-fork-folder-name'); 615 | let forkFolderVerification = ghById('gh-fork-verification') 616 | let forkOkayButton = ghById('gh-fork-folder-okay'); 617 | let forkReset = ghById('gh-fork-reset-button'); 618 | let forkButton = ghById('gh-fork-button'); 619 | let loginError = ghById('gh-fork-log-error'); 620 | let forkMessage = ghById('gh-fork-message-box'); 621 | let mainOwner = ''; 622 | 623 | let logCheck = await ghVerifyGitHubAccount(); 624 | 625 | if (logCheck !== true) { 626 | loginError.style.visibility = 'visible'; 627 | } else { 628 | loginError.style.visibility = 'hidden'; 629 | mainOwner = localStorage.getItem('gh-settings-user-name') 630 | } 631 | 632 | forkMessage.innerHTML = 'No message at this time
'; 633 | 634 | forkSaveFolderButton.addEventListener('click', () => { 635 | openFolderExplorer(window, (selection) => { 636 | forkSaveLocation.innerHTML = selection; 637 | }); 638 | }); 639 | 640 | forkNameField.addEventListener('input', () => forkNewFolderName.value = forkNameField.value); 641 | 642 | forkReset.addEventListener('click', () => { 643 | forkMessage.innerHTML = 'No message at this time
'; 644 | }); 645 | 646 | closeButton.addEventListener('click', () => { 647 | forkReset.click(); 648 | }); 649 | 650 | forkButton.addEventListener('click', async () => { 651 | if (logCheck !== true) return; 652 | if (forkOwner.value === '' || forkNameField === '') return; 653 | 654 | let octokit = await ghCreateOctokitInstance(); 655 | 656 | let newFork = await octokit.rest.repos.createFork({ 657 | owner: forkOwner.value, 658 | repo: forkNameField.value 659 | }); 660 | ghStatusUpdate(newFork.status, forkMessage, 'Repo forked - creating local clone.'); 661 | 662 | let dir = Path.join(forkSaveLocation.innerHTML, forkNewFolderName); 663 | let dirCheck = fse.existsSync(dir); 664 | if (dirCheck) { 665 | forkFolderVerification.classList.add('visible'); 666 | cloneMessage.innerHTML = "Please verify existing folder overwrite.
"; 667 | } else { 668 | let url = "https://github.com/" + mainOwner + "/" + forkNameField.value + '.git'; 669 | const onAuth = () => ({ 670 | username: mainOwner, 671 | password: localStorage.getItem('gh-settings-token') 672 | }); 673 | let forkRepo = await ghClone(onAuth, url, dir, forkMessage, ''); 674 | if (forkRepo) { 675 | let successMessage = document.createElement('p'); 676 | successMessage.innerHTML = 'Local copy created'; 677 | forkMessage.appendChild(successMessage); 678 | let projectData = { 679 | repo: forkNameField.value, 680 | owner: mainOwner, 681 | branch: 'main' 682 | }; 683 | ghCreateJsonFile(projectData, dir, 'githubinfo.json'); 684 | ghWriteGitConfig(dir); 685 | let content = '_pgbackup'; 686 | let ignoreFile = Path.join(dir, '.gitignore'); 687 | try { 688 | const gitIgnore = fse.writeFileSync(ignoreFile, content); 689 | } catch (err) { 690 | console.error(err); 691 | } 692 | } 693 | } 694 | 695 | forkOkayButton.addEventListener('click', async () => { 696 | cloneMessage.innerHTML = ''; 697 | let forkDir = Path.join(forkSaveLocation.innerHTML, forkNewFolderName); 698 | let url = "https://github.com/" + mainOwner + "/" + forkNameField.value + '.git'; 699 | const onAuth = () => ({ 700 | username: mainOwner, 701 | password: localStorage.getItem('gh-settings-token') 702 | }); 703 | let forkRepo = await ghClone(onAuth, url, forkDir, forkMessage, ''); 704 | if (forkRepo) { 705 | let successMessage = document.createElement('p'); 706 | successMessage.innerHTML = 'Local copy created'; 707 | forkMessage.appendChild(successMessage); 708 | let projectData = { 709 | repo: forkNameField.value, 710 | owner: mainOwner, 711 | branch: 'main' 712 | }; 713 | ghCreateJsonFile(projectData, forkDir, 'githubinfo.json'); 714 | ghWriteGitConfig(forkDir); 715 | let content = '_pgbackup'; 716 | let ignoreFile = Path.join(forkDir, '.gitignore'); 717 | try { 718 | const gitIgnore = fse.writeFileSync(ignoreFile, content); 719 | } catch (err) { 720 | console.error(err); 721 | } 722 | } 723 | }); 724 | }) 725 | }; 726 | 727 | //Function to gather staging and commit information. 728 | let ghManipulateCommitFields = () => { 729 | let projectDirectory = ghProjectDirectory(); 730 | let gitStatus = ghById('gh-commit-git-status-box'); 731 | let statusButton = ghById('gh-status-button'); 732 | let commitMessageBox = ghById('gh-commit-message-box'); 733 | let directoryHead = ghById('ghProjectTree'); 734 | let directoryFolders = directoryHead.querySelectorAll('[data-gh-type="folder"]'); 735 | directoryFolders.forEach(folder => { 736 | let folderIcon = folder.querySelector('i'); 737 | folderIcon.addEventListener('click', function (evt) { 738 | folderIcon.classList.contains('icon-right') ? folderIcon.classList.replace('icon-right', 'icon-down') : folderIcon.classList.replace('icon-down', 'icon-right'); 739 | let fileList = folder.querySelector('ul'); 740 | fileList.classList.contains('gh-hidden') ? fileList.classList.replace('gh-hidden', 'gh-visible') : fileList.classList.replace('gh-visible', 'gh-hidden'); 741 | }) 742 | let folderInput = folder.querySelector('input'); 743 | folderInput.addEventListener('click', (evt) => { 744 | let fileList = folder.querySelector('ul'); 745 | let fileInputs = fileList.querySelectorAll('li > input'); 746 | let checkStatus = folderInput.checked; 747 | fileInputs.forEach(input => { 748 | input.checked = checkStatus ? true : false; 749 | }); 750 | }); 751 | }); 752 | let messageError = ghById('gh-commit-message-error'); 753 | let emailError = ghById('gh-commit-email-error'); 754 | let credentialsError = ghById('gh-commit-credentials-error'); 755 | let commitMessage = ghById('gh-commit-message'); 756 | let authorEmail = ghById('gh-commit-email'); 757 | let unstageButton = ghById('gh-unstage-button'); 758 | let stageButton = ghById('gh-stage-button'); 759 | let commitButton = ghById('gh-commit-button'); 760 | let pushButton = ghById('gh-push-button'); 761 | let currentCommitStatus = localStorage.getItem('gh-commited'); 762 | if (currentCommitStatus === true) { 763 | commitButton.disabled = false; 764 | commitMessage.disabled = false; 765 | authorEmail.disabled = false; 766 | } 767 | 768 | 769 | statusButton.addEventListener('click', async () => { 770 | let stagedFiles = await ghStagedFiles(); 771 | gitStatus.innerHTML = 'Currently Staged Files:
' + stagedFiles; 772 | }); 773 | 774 | stageButton.addEventListener('click', async () => { 775 | let filesToStage = ghGetFiles(directoryHead); 776 | let stageFiles = await ghStageFiles(filesToStage); 777 | if (stageFiles) { 778 | let stagedFiles = await ghStagedFiles(); 779 | gitStatus.innerHTML = 'Currently Staged Files:
' + stagedFiles; 780 | localStorage.setItem('gh-commited', true); 781 | commitButton.disabled = false; 782 | commitMessage.disabled = false; 783 | authorEmail.disabled = false; 784 | } 785 | }); 786 | 787 | unstageButton.addEventListener('click', async () => { 788 | let filesToUnstage = ghGetFiles(directoryHead); 789 | let unstageFiles = await ghUnstageFiles(filesToUnstage); 790 | if (unstageFiles) { 791 | let stagedFiles = await ghStagedFiles(); 792 | gitStatus.innerHTML = 'Currently Staged Files:
' + stagedFiles; 793 | } 794 | }); 795 | 796 | commitButton.addEventListener('click', async () => { 797 | let projectDirectory = ghProjectDirectory(); 798 | if (commitMessage.value === '') { 799 | messageError.style.display = "block"; 800 | return; 801 | } 802 | let errorCheck = await ghVerifyGitHubAccount(); 803 | if (!errorCheck) { 804 | credentialsError.style.display = "block"; 805 | return; 806 | } 807 | if (authorEmail.value === '') { 808 | emailError.style.display = "block"; 809 | return; 810 | } 811 | let sha = await Git.commit({ 812 | fs: fse, 813 | dir: projectDirectory, 814 | author: { 815 | name: localStorage.getItem('gh-settings-user-name'), 816 | email: authorEmail.value 817 | }, 818 | message: commitMessage.value 819 | }); 820 | if (sha) { 821 | let content = { 822 | "committed": true 823 | }; 824 | let filename = "githubinfo.json"; 825 | let appendCommit = ghAppendJsonFile(content, projectDirectory, filename); 826 | } 827 | commitMessageBox.innerHTML = "Files committed by " + localStorage.getItem('gh-settings-user-name') + " as SHA " + sha + "
"; 828 | }); 829 | 830 | pushButton.addEventListener('click', async () => { 831 | let projectDirectory = ghProjectDirectory(); 832 | let errorCheck = await ghVerifyGitHubAccount(); 833 | if (!errorCheck) { 834 | credentialsError.classList = 'gh-visible gh-error'; 835 | return; 836 | } 837 | let jsonData = ghReadJsonFile(projectDirectory, 'githubinfo.json'); 838 | if (!jsonData.committed) { 839 | commitMessageBox.innerHTML = "No files committed. Please make a commit and try again.
"; 840 | return; 841 | } 842 | const onAuth = () => ({ 843 | username: localStorage.getItem('gh-settings-user-name'), 844 | password: localStorage.getItem('gh-settings-token') 845 | }); 846 | let pushRequest = await Git.push({ 847 | fs: fse, 848 | http, 849 | dir: projectDirectory, 850 | onAuth, 851 | author: { 852 | name: localStorage.getItem('gh-settings-user-name') 853 | } 854 | }); 855 | let content = { 856 | "committed": false 857 | }; 858 | ghAppendJsonFile(content, projectDirectory, 'githubinfo.json'); 859 | }); 860 | 861 | let cancelButton = ghById('gh-commit-cancel'); 862 | let closeButton = ghById('gh-commit-x'); 863 | let resetModal = () => { 864 | let commitDynamicContainer = ghById('gh-commit-dynamic-container'); 865 | if (commitDynamicContainer) commitDynamicContainer.remove(); 866 | messageError.style.display = "none"; 867 | credentialsError.style.display = "none"; 868 | emailError.style.display = "none"; 869 | gitStatus.innerHTML = ''; 870 | commitMessage.value = ''; 871 | authorEmail.value = ''; 872 | }; 873 | [cancelButton, closeButton].forEach(button => { 874 | button.addEventListener('click', resetModal); 875 | }); 876 | cancelButton.addEventListener('click', () => { 877 | closeButton.removeEventListener('click', resetModal); 878 | }); 879 | closeButton.addEventListener('click', () => { 880 | cancelButton.removeEventListener('click', resetModal); 881 | }); 882 | }; 883 | 884 | let ghManipulatePullFields = () => { 885 | let projectDirectory = ghProjectDirectory(); 886 | let messageArea = ghById('gh-pull-message-box'); 887 | let pullButton = ghById('gh-pull-button'); 888 | let closeX = ghById('gh-pull-x'); 889 | let cancelButton = ghById('gh-pull-cancel'); 890 | let pullIt = () => { 891 | return Git.pull({ 892 | fs: fse, 893 | http, 894 | dir: projectDirectory, 895 | singleBranch: true 896 | }) 897 | .then(() => messageArea.innerHTML = ('Pull successful')) 898 | .catch(err => { 899 | console.error(err); 900 | }); 901 | }; 902 | pullButton.addEventListener('click', pullIt); 903 | closeX.addEventListener('click', () => { 904 | cancelButton.click(); 905 | }); 906 | cancelButton.addEventListener('click', () => { 907 | messageArea.innerHTML = ""; 908 | }) 909 | 910 | } 911 | 912 | //Function to add all the initial field listeners to each modal 913 | let ghAddFieldsListeners = () => { 914 | ghManipulateSettingsFields(); 915 | ghManipulateCreateFields(); 916 | ghManipulateCloneFields(); 917 | ghManipulateBranchFields(); 918 | ghManipulateForkFields(); 919 | } 920 | 921 | //Function to add the click listeners to the initial menu items 922 | let ghAddModalListener = ({ 923 | targetId, 924 | modalName 925 | }) => { 926 | //Adds the click listener to the settings menu item 927 | let modalItem = ghById(targetId); 928 | let modalId = '#gh-' + modalName + '-modal'; 929 | 930 | modalItem.addEventListener('click', () => $(modalId).modal('show')); 931 | }; 932 | 933 | 934 | let ghGetFileAsBASE64 = (filePath) => fse.readFile(filePath, 'base64'); 935 | 936 | let ghSetBranchToCommit = ({ 937 | octokit, 938 | owner, 939 | repo, 940 | currentBranch 941 | }, commitSha) => octokit.git.updateRef({ 942 | owner, 943 | repo, 944 | ref: `heads/${currentBranch}`, 945 | sha: commitSha 946 | }); 947 | 948 | 949 | //HTML for the main menu 950 | let $menu = $(` 951 |