├── .bithoundrc ├── .gitignore ├── .meteor ├── .finished-upgraders ├── .gitignore ├── .id ├── identifier ├── packages ├── platforms ├── release └── versions ├── .tern-project ├── .travis.yml ├── Makefile ├── both ├── data.js ├── feed.js ├── files.js ├── github.js └── routing.js ├── buildpack ├── client ├── conf │ ├── conf.html │ └── conf.js ├── edit │ ├── edit.html │ └── edit.js ├── feed │ ├── feed.html │ └── feed.js ├── file │ ├── file.html │ └── file.js ├── interact │ ├── interact.html │ └── interact.js ├── main │ ├── main.html │ ├── main.js │ └── main.styl ├── render │ ├── raw.html │ ├── raw.js │ └── render.html ├── save │ ├── save.html │ └── save.js └── test │ ├── test.html │ └── test.js ├── history.md ├── package.json ├── packages ├── difflib │ ├── difflib-tests.js │ ├── difflib.js │ └── package.js ├── firepad │ ├── firepad-tests.js │ ├── firepad.js │ └── package.js └── git-sync │ ├── git-sync-tests.js │ ├── git-sync.js │ └── package.js ├── private ├── development.json.cast5 └── production.json.cast5 ├── public ├── apprtc.html ├── favicon.ico ├── images │ ├── commit.gif │ ├── commit.png │ ├── editor.gif │ ├── editor.png │ ├── github.gif │ ├── gitlogo.gif │ ├── gitlogo2.gif │ ├── loading.gif │ ├── topguntocat.gif │ ├── visualize.gif │ └── visualize.png ├── javascript │ ├── application.js │ ├── brython.js │ ├── feedback.js │ ├── html2canvas.min.js │ └── interpreter.py └── style │ └── feedback.min.css ├── readme.md └── server ├── accounts.js ├── commits.js ├── files.js ├── github.js ├── issues.js └── setup.js /.bithoundrc: -------------------------------------------------------------------------------- 1 | "ignore": 2 | [ 3 | "public/**.js", 4 | "packages/git-sync/difflib.js", 5 | ] 6 | "test": 7 | [ 8 | "packages/git-sync/git-sync-tests.js", 9 | ] 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | app/.idea 2 | app/.meteor/.* 3 | app/packages/* 4 | app/.tern-project 5 | 6 | .environment 7 | .versions 8 | 9 | lib-cov 10 | *.seed 11 | *.log 12 | *.csv 13 | *.dat 14 | *.out 15 | *.swp 16 | *.swo 17 | *.pid 18 | *.png 19 | *.gz 20 | 21 | pids 22 | logs 23 | results 24 | 25 | old/ 26 | .npm 27 | npm-debug.log 28 | .build* 29 | 30 | .DS_Store 31 | clock.yml 32 | private/*json 33 | node_modules/ 34 | -------------------------------------------------------------------------------- /.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | notices-for-facebook-graph-api-2 9 | 1.2.0-standard-minifiers-package 10 | 1.2.0-meteor-platform-split 11 | 1.2.0-cordova-changes 12 | 1.2.0-breaking-changes 13 | 1.3.0-split-minifiers-package 14 | 1.4.0-remove-old-dev-bundle-link 15 | 1.4.1-add-shell-server-package 16 | 1.4.3-split-account-service-packages 17 | 1.5-add-dynamic-import-package 18 | -------------------------------------------------------------------------------- /.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | k98h8t1rgu8sh13l0wgd 8 | -------------------------------------------------------------------------------- /.meteor/identifier: -------------------------------------------------------------------------------- 1 | 7uyhiiyx4wdbyf3jr -------------------------------------------------------------------------------- /.meteor/packages: -------------------------------------------------------------------------------- 1 | # LOCAL PACKAGES 2 | # 3 | jeremywrnr:git-sync 4 | jeremywrnr:firepad 5 | jeremywrnr:difflib 6 | 7 | # ATMOSPHERE 8 | # 9 | http@1.2.12 10 | stylus@2.513.9 11 | twbs:bootstrap 12 | bruz:github-api 13 | accounts-password@1.4.0 14 | alanning:roles 15 | accounts-github@1.3.0 16 | iron:router 17 | accounts-base@1.3.3 18 | service-configuration@1.0.11 19 | meteorhacks:async 20 | mizzao:jquery-ui 21 | ecmascript@0.8.2 22 | meteor-platform@1.2.6 23 | es5-shim@4.6.15 24 | chrismbeckett:toastr 25 | shell-server@0.2.4 26 | github-config-ui 27 | dynamic-import 28 | -------------------------------------------------------------------------------- /.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.5.2.1 2 | -------------------------------------------------------------------------------- /.meteor/versions: -------------------------------------------------------------------------------- 1 | accounts-base@1.3.3 2 | accounts-github@1.3.0 3 | accounts-oauth@1.1.15 4 | accounts-password@1.4.0 5 | alanning:roles@1.2.16 6 | allow-deny@1.0.9 7 | autoupdate@1.3.12 8 | babel-compiler@6.20.0 9 | babel-runtime@1.0.1 10 | base64@1.0.10 11 | binary-heap@1.0.10 12 | blaze@2.3.2 13 | blaze-tools@1.0.10 14 | boilerplate-generator@1.2.0 15 | bruz:github-api@0.2.4_1 16 | caching-compiler@1.1.9 17 | caching-html-compiler@1.0.7 18 | callback-hook@1.0.10 19 | check@1.2.5 20 | chrismbeckett:toastr@2.1.2_1 21 | ddp@1.3.1 22 | ddp-client@2.1.3 23 | ddp-common@1.2.9 24 | ddp-rate-limiter@1.0.7 25 | ddp-server@2.0.2 26 | deps@1.0.12 27 | diff-sequence@1.0.7 28 | dynamic-import@0.1.3 29 | ecmascript@0.8.3 30 | ecmascript-runtime@0.4.1 31 | ecmascript-runtime-client@0.4.3 32 | ecmascript-runtime-server@0.4.1 33 | ejson@1.0.14 34 | email@1.2.3 35 | es5-shim@4.6.15 36 | fastclick@1.0.13 37 | geojson-utils@1.0.10 38 | github-config-ui@1.0.0 39 | github-oauth@1.2.0 40 | html-tools@1.0.11 41 | htmljs@1.0.11 42 | http@1.2.12 43 | id-map@1.0.9 44 | iron:controller@1.0.12 45 | iron:core@1.0.11 46 | iron:dynamic-template@1.0.12 47 | iron:layout@1.0.12 48 | iron:location@1.0.11 49 | iron:middleware-stack@1.1.0 50 | iron:router@1.1.2 51 | iron:url@1.1.0 52 | jeremywrnr:difflib@1.0.0 53 | jeremywrnr:firepad@1.0.0 54 | jeremywrnr:git-sync@1.0.2 55 | jquery@1.11.10 56 | launch-screen@1.1.1 57 | livedata@1.0.18 58 | localstorage@1.1.1 59 | logging@1.1.17 60 | meteor@1.7.2 61 | meteor-platform@1.2.6 62 | meteorhacks:async@1.0.0 63 | minimongo@1.3.2 64 | mizzao:build-fetcher@0.3.2 65 | mizzao:jquery-ui@1.11.4 66 | mobile-status-bar@1.0.14 67 | modules@0.10.0 68 | modules-runtime@0.8.0 69 | mongo@1.2.2 70 | mongo-dev-server@1.0.1 71 | mongo-id@1.0.6 72 | npm-bcrypt@0.9.3 73 | npm-mongo@2.2.30 74 | oauth@1.1.13 75 | oauth2@1.1.11 76 | observe-sequence@1.0.16 77 | ordered-dict@1.0.9 78 | promise@0.9.0 79 | random@1.0.10 80 | rate-limit@1.0.8 81 | reactive-dict@1.1.9 82 | reactive-var@1.0.11 83 | reload@1.1.11 84 | retry@1.0.9 85 | routepolicy@1.0.12 86 | service-configuration@1.0.11 87 | session@1.1.7 88 | sha@1.0.9 89 | shell-server@0.2.4 90 | spacebars@1.0.15 91 | spacebars-compiler@1.1.3 92 | srp@1.0.10 93 | stylus@2.513.9 94 | templating@1.2.15 95 | templating-compiler@1.2.15 96 | templating-runtime@1.2.15 97 | templating-tools@1.1.2 98 | tracker@1.1.3 99 | twbs:bootstrap@3.3.6 100 | ui@1.0.13 101 | underscore@1.0.10 102 | url@1.1.0 103 | webapp@1.3.19 104 | webapp-hashing@1.0.9 105 | -------------------------------------------------------------------------------- /.tern-project: -------------------------------------------------------------------------------- 1 | { 2 | "libs": [ 3 | "browser", 4 | "jquery", 5 | "underscore" 6 | ], 7 | "loadEagerly": [ "*.js", "*/*.js", "*/*/*.js", "*/*/*/*.js" ], 8 | "dontLoad": [ ".meteor" ], 9 | "plugins": { 10 | "meteor": {} 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | language: node_js 3 | services: mongodb 4 | node_js: 0.10 5 | 6 | # prereq for getting addon 7 | env: CXX=g++-4.8 8 | addons: 9 | apt: 10 | sources: 11 | - ubuntu-toolchain-r-test 12 | packages: 13 | - g++-4.8 14 | 15 | before_install: 16 | - curl -L https://install.meteor.com | /bin/sh 17 | - curl -L https://git.io/ejPSng | /bin/sh 18 | - export PATH="$HOME/.meteor:$PATH" 19 | - npm install -g spacejam 20 | - meteor --version 21 | - make decrypt 22 | 23 | script: 24 | - spacejam test-packages ./packages/firepad 25 | - spacejam test-packages ./packages/difflib 26 | #- spacejam test-packages ./packages/git-sync 27 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | spacejam test-packages ./packages/firepad 3 | spacejam test-packages ./packages/difflib 4 | #spacejam test-packages ./packages/git-sync 5 | 6 | check: 7 | source .environment || true 8 | 9 | mongo: check 10 | mongo ds027835.mlab.com:27835/heroku_m6rqc1mh -u ${ML_USER} -p ${ML_PASS} 11 | 12 | lines: 13 | find ./* -type f | grep -v jpg | grep -v gif | grep -v md | grep -v ico | xargs wc -l 14 | 15 | # automatically decrypt based on environment variable 16 | # http://ejohn.org/blog/keeping-passwords-in-source-control/ 17 | 18 | AUTHOR=jeremywrnr@gmail.com 19 | CONF1=private/development.json 20 | CONF2=private/production.json 21 | 22 | decrypt: 23 | @echo "Please contact ${AUTHOR} for the encryption password." 24 | openssl cast5-cbc -d -in ${CONF1}.cast5 -out ${CONF1} -pass env:GH_PASSWORD 25 | openssl cast5-cbc -d -in ${CONF2}.cast5 -out ${CONF2} -pass env:GH_PASSWORD 26 | chmod 600 ${CONF1} ${CONF2} 27 | 28 | encrypt: 29 | openssl cast5-cbc -e -in ${CONF1} -out ${CONF1}.cast5 -pass env:GH_PASSWORD 30 | openssl cast5-cbc -e -in ${CONF2} -out ${CONF2}.cast5 -pass env:GH_PASSWORD 31 | 32 | .PHONY: test mongo check lines decrypt encrypt 33 | -------------------------------------------------------------------------------- /both/data.js: -------------------------------------------------------------------------------- 1 | // meteor mongo data publishing 2 | 3 | Files = new Mongo.Collection('files'); // used with github 4 | 5 | /* _id - unique identifier, same as Doc._id 6 | repo - unique identifier of repo file belongs to 7 | branch - name of the branch it is from 8 | title - name of the file 9 | cache - latest version of commit, for diff 10 | content - live content of the file 11 | type - file, nullmode, or image 12 | html - link to file page on github 13 | raw - link to raw file content (github) 14 | */ 15 | 16 | Messages = new Mongo.Collection('messages'); // client side feed 17 | 18 | /* _id - unique identifier of message 19 | repo - unique identifier of repo file belongs to 20 | name - login name of message creator 21 | message - feed item content 22 | time - message creation time 23 | */ 24 | 25 | Issues = new Mongo.Collection('issues'); 26 | 27 | /* _id - unique identifier of task 28 | repo - unique identifier of issue ask belongs to 29 | id - github assigned id issue, also unique 30 | issue - response from server 31 | screen - id of the screenshot 32 | feedback - param from feedback.js 33 | */ 34 | 35 | Screens = new Mongo.Collection('screens'); 36 | 37 | /* _id - unique identifier of commit 38 | img - img data of screen, base64 encoded 39 | */ 40 | 41 | Commits = new Mongo.Collection('commits'); 42 | 43 | /* _id - unique identifier of commit 44 | repo - unique identifier of repo file belongs to 45 | commit - blob from git// TODO: TRIM THIS DOCUMENT!!! 46 | *MORE*... 47 | */ 48 | 49 | Repos = new Mongo.Collection('repos'); // PROJECT ID 50 | 51 | /* _id - unique identifier of commit 52 | sha - git hash code for this commit 53 | users - array of user ids that can push 54 | branches - array of branches (see below) 55 | repo - unique identifier of repo file belongs to 56 | // Branch (inside) 57 | _id - unique identifier of commit 58 | repo - unique identifier of repo file belongs to 59 | sha - git hash code for this commit 60 | */ 61 | 62 | // repo.repo: TODO TRIM THIS DOCUMENT!!! 63 | 64 | //{ _id: 'cRwN6pXzev5wN6B7t', 65 | //user: 'ZGW8J85xAeWLYprXZ', 66 | //repo: 67 | //{ id: 18892802, 68 | //name: 'ECE222Final', 69 | //full_name: 'edsammy/ECE222Final', 70 | //owner: 71 | //{ login: 'edsammy', 72 | //id: 1179387, 73 | //avatar_url: 'https://avatars.githubusercontent.com/u/1179387?v=3', 74 | //gravatar_id: '', 75 | //url: 'https://api.github.com/users/edsammy', 76 | //html_url: 'https://github.com/edsammy', 77 | //followers_url: 'https://api.github.com/users/edsammy/followers', 78 | //following_url: 'https://api.github.com/users/edsammy/following{/other_user}', 79 | //gists_url: 'https://api.github.com/users/edsammy/gists{/gist_id}', 80 | //starred_url: 'https://api.github.com/users/edsammy/starred{/owner}{/repo}', 81 | //subscriptions_url: 'https://api.github.com/users/edsammy/subscriptions', 82 | //organizations_url: 'https://api.github.com/users/edsammy/orgs', 83 | //repos_url: 'https://api.github.com/users/edsammy/repos', 84 | //events_url: 'https://api.github.com/users/edsammy/events{/privacy}', 85 | //received_events_url: 'https://api.github.com/users/edsammy/received_events', 86 | //type: 'User', 87 | //site_admin: false }, 88 | //private: false, 89 | //html_url: 'https://github.com/edsammy/ECE222Final', 90 | //description: 'Final project for ECE 222 at the University of Rochester. Checkout pdf for parameters.', 91 | //fork: false, 92 | //url: 'https://api.github.com/repos/edsammy/ECE222Final', 93 | //forks_url: 'https://api.github.com/repos/edsammy/ECE222Final/forks', 94 | //keys_url: 'https://api.github.com/repos/edsammy/ECE222Final/keys{/key_id}', 95 | //collaborators_url: 'https://api.github.com/repos/edsammy/ECE222Final/collaborators{/collaborator}', 96 | //teams_url: 'https://api.github.com/repos/edsammy/ECE222Final/teams', 97 | //hooks_url: 'https://api.github.com/repos/edsammy/ECE222Final/hooks', 98 | //issue_events_url: 'https://api.github.com/repos/edsammy/ECE222Final/issues/events{/number}', 99 | //events_url: 'https://api.github.com/repos/edsammy/ECE222Final/events', 100 | //assignees_url: 'https://api.github.com/repos/edsammy/ECE222Final/assignees{/user}', 101 | //branches_url: 'https://api.github.com/repos/edsammy/ECE222Final/branches{/branch}', 102 | //tags_url: 'https://api.github.com/repos/edsammy/ECE222Final/tags', 103 | //blobs_url: 'https://api.github.com/repos/edsammy/ECE222Final/git/blobs{/sha}', 104 | //git_tags_url: 'https://api.github.com/repos/edsammy/ECE222Final/git/tags{/sha}', 105 | //git_refs_url: 'https://api.github.com/repos/edsammy/ECE222Final/git/refs{/sha}', 106 | //trees_url: 'https://api.github.com/repos/edsammy/ECE222Final/git/trees{/sha}', 107 | //statuses_url: 'https://api.github.com/repos/edsammy/ECE222Final/statuses/{sha}', 108 | //languages_url: 'https://api.github.com/repos/edsammy/ECE222Final/languages', 109 | //stargazers_url: 'https://api.github.com/repos/edsammy/ECE222Final/stargazers', 110 | //contributors_url: 'https://api.github.com/repos/edsammy/ECE222Final/contributors', 111 | //subscribers_url: 'https://api.github.com/repos/edsammy/ECE222Final/subscribers', 112 | //subscription_url: 'https://api.github.com/repos/edsammy/ECE222Final/subscription', 113 | //commits_url: 'https://api.github.com/repos/edsammy/ECE222Final/commits{/sha}', 114 | //git_commits_url: 'https://api.github.com/repos/edsammy/ECE222Final/git/commits{/sha}', 115 | //comments_url: 'https://api.github.com/repos/edsammy/ECE222Final/comments{/number}', 116 | //issue_comment_url: 'https://api.github.com/repos/edsammy/ECE222Final/issues/comments{/number}', 117 | //contents_url: 'https://api.github.com/repos/edsammy/ECE222Final/contents/{+path}', 118 | //compare_url: 'https://api.github.com/repos/edsammy/ECE222Final/compare/{base}...{head}', 119 | //merges_url: 'https://api.github.com/repos/edsammy/ECE222Final/merges', 120 | //archive_url: 'https://api.github.com/repos/edsammy/ECE222Final/{archive_format}{/ref}', 121 | //downloads_url: 'https://api.github.com/repos/edsammy/ECE222Final/downloads', 122 | //issues_url: 'https://api.github.com/repos/edsammy/ECE222Final/issues{/number}', 123 | //pulls_url: 'https://api.github.com/repos/edsammy/ECE222Final/pulls{/number}', 124 | //milestones_url: 'https://api.github.com/repos/edsammy/ECE222Final/milestones{/number}', 125 | //notifications_url: 'https://api.github.com/repos/edsammy/ECE222Final/notifications{?since,all,participating}', 126 | //labels_url: 'https://api.github.com/repos/edsammy/ECE222Final/labels{/name}', 127 | //releases_url: 'https://api.github.com/repos/edsammy/ECE222Final/releases{/id}', 128 | //created_at: '2014-04-17T20:39:41Z', 129 | //updated_at: '2014-05-28T09:19:56Z', 130 | //pushed_at: '2014-05-28T09:19:57Z', 131 | //git_url: 'git://github.com/edsammy/ECE222Final.git', 132 | //ssh_url: 'git@github.com:edsammy/ECE222Final.git', 133 | //clone_url: 'https://github.com/edsammy/ECE222Final.git', 134 | //svn_url: 'https://github.com/edsammy/ECE222Final', 135 | //homepage: null, 136 | //size: 6652, 137 | //stargazers_count: 0, 138 | //watchers_count: 0, 139 | //language: 'SourcePawn', 140 | //has_issues: true, 141 | //has_downloads: true, 142 | //has_wiki: true, 143 | //has_pages: false, 144 | //forks_count: 0, 145 | //mirror_url: null, 146 | //open_issues_count: 0, 147 | //forks: 0, 148 | //open_issues: 0, 149 | //watchers: 0, 150 | //default_branch: 'master', 151 | //permissions: 152 | //{ admin: false, 153 | //push: true, 154 | //pull: true } } } 155 | 156 | -------------------------------------------------------------------------------- /both/feed.js: -------------------------------------------------------------------------------- 1 | // common (server and client) feed methods 2 | 3 | Meteor.methods({ 4 | 5 | ////////////////// 6 | // FEED MANAGEMENT 7 | ////////////////// 8 | 9 | addMessage(msg) { // add a generic message to the activity feed 10 | if (msg.length) { 11 | Messages.insert({ 12 | owner: Meteor.userId(), 13 | repo: Meteor.user().profile.repo, 14 | name: Meteor.user().profile.login, 15 | time: Date.now(), 16 | message: msg, 17 | }); 18 | 19 | // scroll to the bottom of the feed 20 | if(Meteor.isClient) 21 | $("#feed").stop().animate({ scrollTop: $("#feed")[0].scrollHeight }, 500); 22 | } else 23 | throw new Meteor.Error("null-message"); // passed in empty message 24 | }, 25 | 26 | addUserMessage(usr, msg) { // add message, with userId() (issues) 27 | const poster = Meteor.users.findOne(usr); 28 | if (msg.value !== "") { 29 | if (poster) { 30 | Messages.insert({ 31 | owner: poster._id, 32 | repo: poster.profile.repo, 33 | name: poster.profile.login, 34 | time: Date.now(), 35 | message: msg, 36 | }); 37 | } else 38 | throw new Meteor.Error("null-poster"); // user account is not in mongo 39 | } else 40 | throw new Meteor.Error("null-message"); // they passed in empty message 41 | }, 42 | 43 | }); 44 | -------------------------------------------------------------------------------- /both/files.js: -------------------------------------------------------------------------------- 1 | // common (server and client) file and role methods 2 | 3 | Meteor.methods({ 4 | 5 | ////////////////// 6 | // FILE MANAGEMENT 7 | ////////////////// 8 | 9 | updateFile(id, txt) { // updating files from firepad snapshot 10 | Files.update(id, {$set: { content: txt }}); 11 | }, 12 | 13 | setPilot() { // change the current users profile.role to pilot 14 | return Meteor.users.update( 15 | {"_id": Meteor.userId()}, 16 | {$set : {"profile.role":"pilot"}} 17 | ); 18 | }, 19 | 20 | setCopilot() { // change the current users profile.role to pilot 21 | return Meteor.users.update( 22 | {"_id": Meteor.userId()}, 23 | {$set : {"profile.role":"copilot"}} 24 | ); 25 | }, 26 | 27 | }); 28 | -------------------------------------------------------------------------------- /both/github.js: -------------------------------------------------------------------------------- 1 | // common (server and client) github methods 2 | 3 | Meteor.methods({ 4 | 5 | ////////////////// 6 | // REPO MANAGEMENT 7 | ////////////////// 8 | 9 | updateRepo() { // update when repo was last updated 10 | return Meteor.users.update( 11 | {"_id": Meteor.userId()}, 12 | {$set : { 13 | "profile.lastUpdated": new Date(), 14 | }}); 15 | }, 16 | 17 | loadRepo(gr) { // load a repo into code pilot 18 | Meteor.call("setRepo", gr); // set the active project / repo 19 | Meteor.call("initBranches", gr); // get all the possible branches 20 | const branch = gr.repo.default_branch; 21 | Meteor.call("setBranch", branch); // set branch 22 | Meteor.call("initCommits"); // pull commit history for gr repo 23 | 24 | // if has loaded files, then just set the repo 25 | var anyFile = Files.findOne({repo: gr._id}) 26 | if (anyFile) return true; 27 | 28 | Meteor.call("loadHead", branch); // load the head of gr branch into CP 29 | const full = `${gr.repo.owner.login}/${gr.repo.name}`; 30 | Meteor.call("addMessage", `started working on repo - ${full}`); 31 | }, 32 | 33 | setRepo(gr) { // set git repo & default branch 34 | return Meteor.users.update( 35 | {"_id": Meteor.userId()}, 36 | {$set : { 37 | "profile.repo": gr._id, 38 | "profile.repoName": gr.repo.name, 39 | "profile.repoOwner": gr.repo.owner.login, 40 | "profile.repoBranch": gr.repo.owner.default_branch 41 | }}); 42 | }, 43 | 44 | forkRepo(user, repo) { // create a fork 45 | try { // if the repo exists/isForkable 46 | Meteor.call("getRepo", user, repo); 47 | 48 | // try to post a forked version on GH 49 | Meteor.call("postRepo", user, repo); 50 | 51 | // pull in the forked version 52 | Meteor.call("getAllRepos"); 53 | } catch (err) { // this repo won't no fork 54 | toastr.error(`couldn't fork repo '${repo}'`); 55 | } 56 | }, 57 | 58 | 59 | 60 | //////////////////// 61 | // BRANCH MANAGEMENT 62 | //////////////////// 63 | 64 | // for the current repo, just overwrite branches with new 65 | initBranches(gr) { // get all branches for this repo 66 | const brs = Meteor.call("getBranches", gr); // res from github 67 | Repos.update(gr._id, { $set: {branches: brs }}); 68 | }, 69 | 70 | addBranch(bn) { // create a new branch from branchname (bn) 71 | const repo = Repos.findOne(Meteor.user().profile.repo); 72 | const branch = Meteor.user().profile.repoBranch; 73 | const parent = Meteor.call("getBranch", branch).commit.sha; 74 | const newBranch = Meteor.call("postBranch", bn, parent); 75 | Meteor.call("initBranches", repo); 76 | Meteor.call("setBranch", bn); 77 | Meteor.call("addMessage", `created branch - ${bn}`); 78 | }, 79 | 80 | loadBranch(bn) { // load a repo into code pilot 81 | Meteor.call("setBranch", bn); // set branch for current user 82 | Meteor.call("initCommits"); // pull commit history for this repo 83 | Meteor.call("loadHead", bn); // load the head of this branch into CP 84 | Meteor.call("addMessage", `started working on branch - ${bn}`); 85 | }, 86 | 87 | setBranch(bn) { // set branch name 88 | return Meteor.users.update( 89 | {"_id": Meteor.userId()}, 90 | {$set : { 91 | "profile.repoBranch": bn, 92 | }}); 93 | }, 94 | }); 95 | 96 | -------------------------------------------------------------------------------- /both/routing.js: -------------------------------------------------------------------------------- 1 | // routing for GitSync 2 | 3 | Router.configure({ layoutTemplate: "main" }); 4 | 5 | Router.route("/", function() { 6 | this.render("code"); 7 | }); 8 | 9 | Router.map(function() { 10 | this.route("code"); 11 | this.route("test"); 12 | this.route("save"); 13 | this.route("login"); 14 | this.route("config"); 15 | this.route("raw", { layoutTemplate: "null" }); 16 | this.route("renderer", { layoutTemplate: "null" }); 17 | this.route("interactJs", { layoutTemplate: "null" }); 18 | this.route("interactPy", { layoutTemplate: "null" }); 19 | this.route("interactRuby", { layoutTemplate: "null" }); 20 | }); 21 | 22 | 23 | // accepting screenshots at the feedback url 24 | // curl --data "lat=12&lon=14" http://localhost:3000/feedback 25 | 26 | Router.route("feedback", { 27 | path: "/feedback/", 28 | where: "server", 29 | action: function addIssue() { 30 | const issue = JSON.parse( this.request.body.feedback ); 31 | if(Meteor.users.findOne(issue.user)) // dont take junk 32 | Meteor.call("addIssue", issue); 33 | this.response.statusCode = 201; 34 | this.response.setHeader("Content-Type", "application/json"); 35 | this.response.end("{status: 'added'"); 36 | } 37 | }); 38 | 39 | // serving feedback images and rendered pages, from id 40 | 41 | Router.route("screenshot/:_id", { // serve feedback images 42 | name: "screenshot", 43 | layoutTemplate: "null", 44 | onBeforeAction: null, 45 | action: function viewScreen() { 46 | const img = Screens.findOne(this.params._id); 47 | this.render("screenshot", {data: img}); 48 | } 49 | }); 50 | 51 | 52 | // ask user to login before coding, only on client 53 | 54 | if(Meteor.isClient) { 55 | Router.onBeforeAction(function preLogin() { 56 | if (! Meteor.userId() || Meteor.loggingIn()) 57 | this.render("login"); 58 | else 59 | this.next(); 60 | }, { // but allow anybody to check issue imgs 61 | except: ["login", "screenshot", "rendered"] 62 | }); 63 | } 64 | 65 | -------------------------------------------------------------------------------- /buildpack: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # export github API password for production 4 | export "GH_PASSWORD=$(cat $1/GH_PASSWORD)" 5 | echo "GH PASSWORD EXPORTED ==============" 6 | 7 | # get auth keys 8 | make decrypt 9 | -------------------------------------------------------------------------------- /client/conf/conf.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 81 | 82 | 83 | 105 | 106 | 128 | 129 | 151 | 152 | 153 | 154 | 158 | 159 | 163 | 164 | 165 | 215 | -------------------------------------------------------------------------------- /client/conf/conf.js: -------------------------------------------------------------------------------- 1 | // configuration page 2 | 3 | const prof = GitSync.prof; 4 | const focusForm = GitSync.focusForm; 5 | 6 | Template.account.helpers({ 7 | repos() { 8 | return Repos.find({}, {sort: {"repo.owner": -1, "repo.name": 1}} ); 9 | }, 10 | 11 | branches() { // if there are branches, return them 12 | const repo = Repos.findOne( prof().repo ); 13 | if (!repo) // user has yet to set a repo 14 | return []; 15 | const brs = repo.branches; 16 | if (brs) 17 | return brs; 18 | else // branches havent loaded || something else? 19 | return []; 20 | }, 21 | 22 | userHasRepo() { // empty string is default value for repo // selecting different repo 23 | return (Meteor.user() && Meteor.user().profile.repo != "") 24 | }, 25 | 26 | repoSelecting() { 27 | return Session.equals("focusPane", "repo"); 28 | }, 29 | 30 | branchSelecting() { 31 | return Session.equals("focusPane", "branch"); 32 | }, 33 | 34 | lastUpdated() { 35 | return prof().lastUpdated.toLocaleString(); 36 | }, 37 | }); 38 | 39 | 40 | Template.account.events({ 41 | "click .repoSelect"(e) { // show the available repos 42 | e.preventDefault(); 43 | Session.set("focusPane", "repo"); 44 | if (Repos.find({}).count() === 0) { // if no repos, load them in 45 | Meteor.call("getAllRepos"); 46 | Meteor.call("updateRepo"); 47 | } 48 | }, 49 | 50 | "click .branchSelect"(e) { // show the available branches 51 | e.preventDefault(); 52 | const repo = Repos.findOne(prof().repo); 53 | if (repo) Meteor.call("initBranches", repo); // get all possible branches 54 | Session.set("focusPane", "branch"); 55 | }, 56 | 57 | "click .unfocus"(e) { // hide the available repos 58 | e.preventDefault(); 59 | Session.set("focusPane", null); 60 | Session.set("branching", false); 61 | Session.set("forking", false); 62 | }, 63 | 64 | "click .loadGHData"(e) { // load in repos from github 65 | e.preventDefault(); 66 | Meteor.call("getAllRepos"); 67 | Meteor.call("updateRepo"); 68 | } 69 | }); 70 | 71 | 72 | // repo forking may improve life 73 | 74 | Template.forkRepo.helpers({ 75 | 76 | forking() { 77 | return Session.equals("forking", true); 78 | }, 79 | 80 | }); 81 | 82 | 83 | Template.forkRepo.events({ 84 | 85 | "click .forkrepo"(e) { // display the forking code box 86 | e.preventDefault(); 87 | Session.set("forking", true); 88 | focusForm("#repoForker"); 89 | }, 90 | 91 | "submit .forker"(e) { // fork and load a repo into code pilot 92 | e.preventDefault(); 93 | $(e.target).blur(); // parse string arg for user, repo 94 | //https://babeljs.io/docs/learn-es2015/#destructuring - sadness :((((((((( 95 | //const [user, repo] = $("#repoForker")[0].value.split("/"); // ES6 not working??? 96 | const split = $("#repoForker")[0].value.split("/"); 97 | const user = split[0], repo = split[1]; 98 | const selfFork = (prof().login === user); // cant fork self 99 | 100 | if (split.length !== 2 || !user || !repo || selfFork) 101 | return false; 102 | 103 | Session.set("loadingRepo", true); 104 | Meteor.call("forkRepo", user, repo, function (err, res) { 105 | 106 | // finding repo which was just forked 107 | let fName = prof().login + '/' + repo 108 | let fRepo = Repos.findOne({ "repo.full_name": fName }) 109 | Session.set("forking", false); 110 | console.log(fName) 111 | console.log(fRepo) 112 | if (!fRepo || err) { 113 | Session.set("loadingRepo", false); 114 | return console.error(err) 115 | } 116 | 117 | // set repo to current, stop if error 118 | Meteor.call("setRepo", fRepo, function (e, r) { 119 | if (e) Session.set("loadingRepo", false) 120 | }); 121 | 122 | // load the repo's contents 123 | Meteor.call("loadRepo", fRepo, function () { 124 | Session.set("loadingRepo", false) 125 | }); 126 | }); 127 | }, 128 | 129 | "click .cancelFork"(e) { 130 | Session.set("forking", false); 131 | }, 132 | }); 133 | 134 | 135 | 136 | Template.typeRepo.helpers({ 137 | 138 | typing() { 139 | return Session.equals("typing", true); 140 | }, 141 | 142 | }); 143 | 144 | 145 | Template.typeRepo.events({ 146 | 147 | "click .typerepo"(e) { // display the typing code box 148 | e.preventDefault(); 149 | Session.set("typing", true); 150 | focusForm("#repoTyper"); 151 | }, 152 | 153 | "submit .typer"(e) { // type and load a repo into code pilot 154 | e.preventDefault(); 155 | $(e.target).blur(); 156 | 157 | // parse string arg for user, repo 158 | const full = $("#repoTyper")[0].value, 159 | split = full.split("/"), 160 | user = split[0], 161 | repo = split[1]; 162 | 163 | if (split.length !== 2 || !user || !repo) 164 | return toastr.error("enter repo in the form 'user/repo'"); 165 | 166 | Session.set("loadingRepo", true); 167 | Meteor.call("getRepo", user, repo, function (err, res) { 168 | 169 | // finding repo which was just typed 170 | let fRepo = Repos.findOne({ "repo.full_name": full }) 171 | console.log(full); 172 | console.log(fRepo); 173 | Session.set("typing", false); 174 | if (!fRepo || err) { 175 | Session.set("loadingRepo", false); 176 | toastr.error(`couldn't select repo '${full}'`); 177 | return false; 178 | } 179 | 180 | // set repo to current, stop if error 181 | Meteor.call("setRepo", fRepo, function (e, r) { 182 | if (e) Session.set("loadingRepo", false) 183 | }); 184 | 185 | // load the repo's contents 186 | Meteor.call("loadRepo", fRepo, function () { 187 | Session.set("loadingRepo", false) 188 | }); 189 | }); 190 | }, 191 | 192 | "click .cancelType"(e) { 193 | Session.set("typing", false); 194 | }, 195 | }); 196 | 197 | 198 | 199 | // make branch forking work as well 200 | 201 | Template.newBranch.helpers({ 202 | branching() { 203 | return Session.get("branching"); 204 | }, 205 | 206 | currentBranch() { 207 | return prof().repoBranch; 208 | }, 209 | }); 210 | 211 | 212 | Template.newBranch.events({ 213 | "click .newBranch"(e) { // display the branching code box 214 | e.preventDefault(); 215 | Session.set("branching", true); 216 | focusForm("#brancher"); 217 | }, 218 | 219 | "submit .brancher"(e) { // fork and load a repo into code pilot 220 | e.preventDefault(); 221 | $(e.target).blur(); // parse string arg for user, repo 222 | const branchName = $.trim( $("#branchNamer")[0].value ); 223 | // TODO: check if existing branch, deny 224 | // TODO: check for illegal branchnames 225 | // http://stackoverflow.com/questions/3651860/which-characters-are-illegal-within-a-branch-name 226 | if (branchName.length == 0) return false; 227 | Meteor.call("addBranch", branchName); 228 | Session.set("branching", false); 229 | Session.set("focusPane", null); 230 | }, 231 | 232 | "click .cancelBranch"(e) { 233 | Session.set("branching", false); 234 | }, 235 | }); 236 | 237 | 238 | 239 | // existing git repo and branch handling 240 | 241 | Template.repo.events({ 242 | 243 | "click .repo"(e) { // load a different repo into GitSync 244 | if (prof().repo !== this._id) { // selecting different repo 245 | Session.set("loadingRepo", true) 246 | Meteor.call("loadRepo", this, function () { 247 | Session.set("loadingRepo", false) 248 | }); 249 | } 250 | 251 | Session.set("focusPane", null); 252 | Session.set("testFile", null); 253 | } 254 | 255 | }); 256 | 257 | Template.branch.events({ 258 | 259 | "click .branch"(e) { // load a different branch into GitSync 260 | if (prof().repoBranch !== this.name) 261 | Meteor.call("loadBranch", this.name); 262 | Session.set("focusPane", null); 263 | } 264 | 265 | }); 266 | 267 | Template.extras.events({ 268 | 269 | "click .resetfiles"(e) { // reset to most basic website... 270 | const trulyReset = confirm("This will overwrite any uncommitted changes. Proceed?"); 271 | if (trulyReset) Meteor.call("resetFiles"); 272 | }, 273 | 274 | }); 275 | 276 | -------------------------------------------------------------------------------- /client/edit/edit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 20 | 21 | 22 | 23 | 38 | 39 | 63 | 64 | 67 | -------------------------------------------------------------------------------- /client/edit/edit.js: -------------------------------------------------------------------------------- 1 | // code editor things 2 | 3 | const prof = GitSync.prof; 4 | const ufids = GitSync.ufids; 5 | const imgcheck = GitSync.imgcheck; 6 | const focusForm = GitSync.focusForm; 7 | 8 | const renderEditor = () => { 9 | // deleting old editor 10 | console.log(`rendering: ${Session.get("document")}`) 11 | $("#editor-container").empty(); 12 | $("#editor-container").append("
"); 13 | focusForm("#editor"); 14 | 15 | // avoid first rendering error 16 | if ($("#editor").length === 0) return; 17 | 18 | // make fresh new editor 19 | const editor = ace.edit("editor"); 20 | editor.$blockScrolling = Infinity; 21 | editor.setTheme("ace/theme/monokai"); 22 | editor.setShowPrintMargin(false); 23 | const session = editor.getSession(); 24 | session.setUseWrapMode(true); 25 | session.setUseWorker(false); 26 | focusForm("#editor"); 27 | 28 | // Create Firepad. 29 | const firepadRef = new Firebase(Session.get("firepadRef")); 30 | const firepad = Firepad.fromACE(firepadRef, 31 | editor, { userId: prof().login, }); 32 | 33 | // Get cached content for when history empty 34 | const file = Files.findOne(Session.get("document")); 35 | firepad.on('ready', () => { 36 | if (firepad.isHistoryEmpty() && file.content) 37 | firepad.setText(file.content); 38 | 39 | // Focus the editor panel 40 | editor.focus(); 41 | editor.gotoLine(1); 42 | }); 43 | 44 | // Filemode and suggestions 45 | const mode = GitSync.findFileMode(Session.get("document")); 46 | editor.getSession().setMode(mode); 47 | const beautify = ace.require("ace/ext/beautify"); 48 | editor.commands.addCommands(beautify.commands); 49 | editor.setOptions({ // more editor completion 50 | enableBasicAutocompletion: true, 51 | enableLiveAutocompletion: true, 52 | enableSnippets: true 53 | }); 54 | }; 55 | 56 | /* Odd artifact here - onRendered needs to have the render editor function fed 57 | * into it in order for the firepad to be loaded when returning from another 58 | * view, but will not trigger when the session document updates. To get around 59 | * this, we insert a 'render' helper in the editor template body, inside a with 60 | * docid statement. this handles not updating the firepad when the template is 61 | * the same. first tried using tracker autorun but that was running way to many 62 | * times and was unsure if you could configure it to reload only when the 63 | * active session document works. fails to style content on first load - 64 | * something with not being able to find the editor instance 65 | * */ 66 | 67 | Template.editor.helpers({ 68 | docid() { return Session.get("document"); }, 69 | 70 | render() { renderEditor(); }, // Create ACE editor 71 | 72 | isImage() { // check if file extension is renderable 73 | const file = Files.findOne(Session.get("document")); 74 | if (file) 75 | return imgcheck(file.title) 76 | }, 77 | }); 78 | 79 | Template.editor.onRendered(renderEditor); 80 | 81 | 82 | 83 | Template.filename.helpers({ 84 | rename() { 85 | return Session.equals("focusPane", "renamer"); 86 | }, 87 | 88 | title() { 89 | const ref = Files.findOne(Session.get("document")); 90 | if (ref) return ref.title; 91 | } 92 | }); 93 | 94 | Template.filename.events({ 95 | // rename the current file 96 | "submit .rename"(e) { 97 | e.preventDefault(); 98 | $(e.target).blur(); 99 | const txt = $("#filetitle")[0].value; 100 | if (txt == null || txt == "") return false; 101 | const id = Session.get("document"); 102 | Session.set("focusPane", null); 103 | Meteor.call("renameFile", id, txt); 104 | }, 105 | 106 | // if rename loses focus, stop 107 | "blur #filetitle"(e) { 108 | Session.set("focusPane", null); 109 | }, 110 | 111 | // test the current file 112 | "click .test"(e) { 113 | let doc = Session.get("document") 114 | console.log(`testing: ${doc}`) 115 | Session.set("testViz", true) 116 | Session.set("testFile", doc) 117 | FirepadAPI.getText(doc, function (txt) { 118 | Meteor.call("updateFile", doc, txt); 119 | }); 120 | }, 121 | 122 | // enable changing of filename 123 | "click button.edit"(e) { 124 | e.preventDefault(); 125 | Session.set("focusPane", "renamer"); 126 | focusForm("#filetitle"); 127 | }, 128 | 129 | // delete the current file 130 | "click button.del"(e) { 131 | e.preventDefault(); 132 | const trulyDelete = confirm("This will delete the file. Proceed?"); 133 | 134 | if (trulyDelete) { 135 | const id = Session.get("document"); 136 | Meteor.call("deleteFile", id); 137 | Session.set("focusPane", null); 138 | Session.set("document", null); 139 | } 140 | } 141 | }); 142 | -------------------------------------------------------------------------------- /client/feed/feed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | 21 | 22 | 33 | 34 | 40 | 41 | -------------------------------------------------------------------------------- /client/feed/feed.js: -------------------------------------------------------------------------------- 1 | // messages and events feed 2 | 3 | const linkify = GitSync.linkify; 4 | 5 | Template.chatter.events({ 6 | 7 | "keydown input#message"(e) { 8 | if (e.which === 13) { // "enter" keycode recieved 9 | const msg = $("input#message")[0]; 10 | Meteor.call("addMessage", $.trim(msg.value)); 11 | msg.value = ""; // purge the old message 12 | } 13 | } 14 | 15 | }); 16 | 17 | 18 | Template.messages.helpers({ 19 | messageCount() { // count feed items 20 | return Messages.find({}).count(); 21 | }, 22 | 23 | messages() { // linkify and return feed items 24 | return Messages.find({}, {sort: {time: 1}}); 25 | }, 26 | }); 27 | 28 | 29 | // scroll down on new messages 30 | Template.message.onRendered(() => { 31 | const newFeedCount = Messages.find({}).count(); 32 | const feed = $("#feed")[0]; 33 | 34 | if ((! Session.equals("feedCount", newFeedCount)) && feed) { 35 | $("#feed").stop().animate({ scrollTop: feed.scrollHeight }, 500); 36 | Session.set("feedCount", newFeedCount); 37 | } 38 | 39 | // auto enable bootstrap tooltips 40 | $('[data-toggle="tooltip"]').tooltip({delay: 0}) 41 | }); 42 | 43 | Template.message.helpers({ 44 | linked() { // return local message time 45 | return linkify(this.message); 46 | }, 47 | 48 | timestamp() { // return local message time 49 | const msgdate = new Date(this.time); 50 | return msgdate.toLocaleTimeString().toLowerCase(); 51 | }, 52 | 53 | datestamp() { // return local message date 54 | const msgdate = new Date(this.time); 55 | return msgdate.toLocaleDateString(); 56 | } 57 | }); 58 | -------------------------------------------------------------------------------- /client/file/file.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 36 | 37 | 46 | 47 | 48 | 49 | 57 | 58 | -------------------------------------------------------------------------------- /client/file/file.js: -------------------------------------------------------------------------------- 1 | // file feed helpers 2 | 3 | const newFile = e => { 4 | e.preventDefault(); 5 | Meteor.call("newFile", (err, id) => { 6 | Session.set("document", id); 7 | }); 8 | }; 9 | 10 | Template.filelist.events({ 11 | "click .new"(e) { newFile(e); } 12 | }); 13 | 14 | Template.userfiles.helpers({ 15 | files() { 16 | return Files.find({}, {sort: {"title": 1}} ) 17 | } 18 | }); 19 | 20 | Template.userfiles.events({ 21 | "click .new"(e) { newFile(e); } 22 | }); 23 | 24 | 25 | 26 | // individual files 27 | 28 | Template.fileitem.helpers({ 29 | 30 | current() { 31 | return Session.equals("document", this._id); 32 | } 33 | 34 | }); 35 | 36 | Template.fileitem.events({ 37 | 38 | "click .file"() { 39 | //if (!Session.equals("document", this._id)) 40 | //Meteor.call("addMessage", "opened file " + this.title); 41 | Session.set("firepadRef", Session.get("fb") + this._id); 42 | Session.set("document", this._id); 43 | }, 44 | 45 | }); 46 | -------------------------------------------------------------------------------- /client/interact/interact.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremywrnr/codepilot/ceebac93e97e4553df53d6cdcc616f6bd0efbe56/client/interact/interact.html -------------------------------------------------------------------------------- /client/interact/interact.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremywrnr/codepilot/ceebac93e97e4553df53d6cdcc616f6bd0efbe56/client/interact/interact.js -------------------------------------------------------------------------------- /client/main/main.html: -------------------------------------------------------------------------------- 1 | 2 | git-sync 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 57 | 58 | 64 | 65 | 66 | 67 | 70 | 71 | 78 | 79 | 80 | 81 | 82 | 83 | 97 | 98 | 99 | 100 | 101 | 112 | 113 | 118 | 119 | 122 | 123 | 124 | 125 | 126 | 127 | 149 | 150 | 151 | 198 | -------------------------------------------------------------------------------- /client/main/main.js: -------------------------------------------------------------------------------- 1 | // default session settings 2 | Session.setDefault("feedCount", 0); 3 | Session.setDefault("document", null); 4 | Session.setDefault("focusPane", null); 5 | Session.setDefault("hideClosedIssues", true); 6 | 7 | // todo move these to repo level 8 | Session.setDefault("testViz", true); 9 | Session.setDefault("testInt", false); 10 | Session.setDefault("testWeb", false); 11 | Session.setDefault("testFile", null); 12 | 13 | // checking which firebase to use 14 | Meteor.call("firebase", (err, res) => { 15 | if (!err) Session.set("fb", res) 16 | }); 17 | 18 | 19 | 20 | // startup data subscriptions 21 | 22 | const prof = GitSync.prof; 23 | 24 | Meteor.subscribe("screens"); 25 | Tracker.autorun(() => { // subscribe on login 26 | if (Meteor.user()) { 27 | Meteor.subscribe("repos", Meteor.userId()); 28 | if (prof().repo) { 29 | 30 | const user = prof(); // get user profile 31 | Meteor.subscribe("issues", user.repo); 32 | Meteor.subscribe("messages", user.repo); 33 | 34 | const branch = user.repoBranch; // get branch 35 | Meteor.subscribe("commits", user.repo, branch); 36 | 37 | // TODO get files just that are in this commit (how...) 38 | // probably in files include a title unique marker and then have a dict 39 | // with key values of commit ids and two sub fields - cached and content 40 | Meteor.subscribe("files", user.repo, branch); 41 | } 42 | } 43 | }); 44 | 45 | 46 | 47 | // global client helper(s) 48 | 49 | Template.registerHelper("isPilot", () => { // check if currentUser is pilot 50 | if (!Meteor.user()) return false; // still logging in or page loading 51 | return prof().role === "pilot"; 52 | }); 53 | 54 | Template.registerHelper("nulldoc", () => Session.equals("document", null)); 55 | 56 | Template.registerHelper("nullrepo", () => { // check if currentDoc is null 57 | if (!Meteor.user()) return false; // still logging in or page loading 58 | return !prof().repo; // return true when repo is null 59 | }); 60 | 61 | 62 | 63 | // navbar config 64 | 65 | Template.navigation.helpers({ // uses glyphicons in template 66 | userHasRepo() { // empty string is default value for repo 67 | return (Meteor.user() && Meteor.user().profile.repo != "") 68 | }, 69 | 70 | navItems() { 71 | return [ 72 | { iconpath:"/code", iconname:"pencil", name:"code" }, 73 | { iconpath:"/test", iconname:"search", name:"check" }, 74 | { iconpath:"/save", iconname:"list-alt", name:"commit" } ] } 75 | }); 76 | 77 | // bring renderer to the top of the page 78 | Template.renderer.onRendered(() => { 79 | window.scrollTo(0,0); 80 | }); 81 | 82 | // login setup 83 | 84 | Template.main.helpers({ // check if user has setup repo yet 85 | 86 | userHasRepo() { // empty string is default value for repo 87 | return (Meteor.user() && Meteor.user().profile.repo != "") 88 | }, 89 | 90 | loadingRepo() { 91 | return Session.get("loadingRepo") 92 | }, 93 | 94 | }); 95 | 96 | Template.userLoggedout.events({ 97 | "click .login"(e) { 98 | Meteor.loginWithGithub({ 99 | requestPermissions: ["user", "repo"], 100 | loginStyle: "redirect", 101 | }, err => { 102 | if (err) 103 | Session.set("errorMessage", err.reason); 104 | }); 105 | } 106 | }); 107 | 108 | Template.userLoggedin.events({ 109 | "click .logout"(e) { 110 | Meteor.logout(err => { 111 | if (err) 112 | Session.set("errorMessage", err.reason); 113 | }); 114 | } 115 | }); 116 | -------------------------------------------------------------------------------- /client/main/main.styl: -------------------------------------------------------------------------------- 1 | // variable defs 2 | 3 | lightgreen = rgb(223, 240, 216) 4 | lightred = rgb(242, 222, 222) 5 | lightgray = #f5f5f5 6 | lightblue = #d9edf7 7 | monokai = #2f3129 8 | darkblue = #337AB7 9 | logored = #e95e45 10 | darkgray = #222 11 | midgray = #ddd 12 | 13 | // stylus mixins 14 | 15 | border-radius() 16 | -webkit-border-radius arguments 17 | -moz-border-radius arguments 18 | border-radius arguments 19 | h-center(w) 20 | margin-right auto 21 | margin-left auto 22 | max-width w 23 | width w 24 | v-center(n) 25 | vertical-align middle 26 | margin-bottom 0px 27 | margin-top n 28 | w-content(w) 29 | max-width w 30 | top 50px 31 | width w 32 | heading-style(txt, bg, border) 33 | border-width 1px 0px 0px 0px 34 | background-color bg 35 | border-color border 36 | border-style solid 37 | font-size 16px 38 | height 48px 39 | color txt 40 | 41 | // main stylus 42 | 43 | html 44 | font-family Verdana, sans-serif 45 | html, body 46 | line-height normal 47 | height 100% 48 | h1 > code 49 | background transparent 50 | h5 51 | text-decoration underline 52 | iframe 53 | width 100% 54 | img 55 | max-width 100% 56 | 57 | // main body 58 | 59 | .main-content 60 | padding-bottom 5em 61 | position absolute 62 | w-content(70vw) 63 | left 30vw 64 | .setup 65 | position relative 66 | h-center(600px) 67 | top 40px 68 | .header // colors: txt, bg, border 69 | heading-style(black, lightgray, midgray) 70 | .sidebar 71 | border-right thin dotted darkgray 72 | position fixed 73 | w-content(30vw) 74 | height 100% 75 | left 0px 76 | .navbar-nav 77 | font-size 16px 78 | .navbar-text 79 | v-center(14px) 80 | .navbar-form 81 | v-center(6.5px) 82 | .account 83 | height 100% 84 | .sidelist 85 | max-height 30vh 86 | overflow-y scroll 87 | overflow-x hidden 88 | .nav.sidelist > li.msg 89 | padding 5px 90 | .center 91 | display block 92 | margin auto 93 | .tcenter 94 | text-align center 95 | .round 96 | border-radius(4px) 97 | .hidden 98 | visibility hidden 99 | .wide 100 | max-width 500px 101 | width 100% 102 | .wider 103 | width 100% 104 | .nomargin 105 | margin 0px 106 | .nomargin-image 107 | margin-top -20px 108 | .jumbotron 109 | background lightblue 110 | padding 40px 15vw 20px 15vw 111 | margin-bottom 0px 112 | .welcome>.jumbotron 113 | padding 10px 15vw 16px 15vw 114 | margin-bottom 25px 115 | max-width 100% 116 | .jumbotron > * 117 | margin-right auto 118 | margin-left auto 119 | max-width 750px 120 | .jumbotron > h2 121 | margin-right initial 122 | .panel 123 | .panel-heading 124 | border-width 1px 0px 0px 0px 125 | border-radius 0px 126 | margin-bottom 0px 127 | margin-top 0px 128 | .about-panel 129 | padding 0px 48px 16px 48px 130 | text-align center 131 | line-height normal 132 | font-weight 200 133 | max-width 800px 134 | font-size 21px 135 | display block 136 | margin auto 137 | .alert 138 | .list-group 139 | margin-bottom 0 140 | 141 | // button styling 142 | 143 | a.btn 144 | background buttonface 145 | color rgb(85, 85, 85) 146 | text-decoration none 147 | a.powered-by-firepad // firepad editor 148 | display none 149 | .btn:hover 150 | color white 151 | .grn:hover 152 | background-color green 153 | .edit:hover 154 | background-color orange 155 | .del:hover 156 | background-color red 157 | 158 | // id based styles 159 | 160 | #mainhead // colors: txt, bg, border 161 | heading-style(white, darkblue, white) 162 | #editor-head 163 | heading-style(white, darkblue, white) 164 | position fixed 165 | z-index 10 166 | width 100% 167 | .navbar-form 168 | margin-right 31vw 169 | padding 0px 170 | #editor-container 171 | min-height: calc(100vh - 120px); 172 | overflow hidden 173 | height 83vh 174 | #editor 175 | position absolute 176 | top 48px 177 | bottom 0 178 | right 0 179 | left 0 180 | #feed 181 | padding 10px 5px 182 | #active-file 183 | #active-issue 184 | #active-commit 185 | li.file:hover a 186 | button.list-group-item:hover 187 | background-color lightblue 188 | color darkblue 189 | .github-logo 190 | height 25px 191 | div.setup>h2 192 | font-family monospace 193 | color logored 194 | 195 | // line numbering the diffs 196 | 197 | pre.diff 198 | line-height 1 199 | padding: 5px 200 | border none 201 | margin 0 202 | pre.ins 203 | background lightgreen 204 | pre.del 205 | background lightred 206 | span.lineno 207 | font-weight lighter 208 | font-style italic 209 | margin-left 0.5em 210 | float left 211 | 212 | // jquery ui resizer 213 | 214 | .ui-resizable-s 215 | background midgray 216 | height 25px 217 | bottom 0px 218 | .ui-resizable-s:hover 219 | background lightblue 220 | .ui-resizable-helper 221 | border 2px dotted #00F 222 | 223 | // smaller bootstrap nav 224 | 225 | @media (min-width: 400px) { 226 | .navbar-toggle { 227 | display: none; } 228 | .navbar-collapse { 229 | border-top: 0 none; 230 | box-shadow: none; 231 | width: auto; } 232 | .navbar-collapse.collapse { 233 | display: block !important; 234 | height: auto !important; 235 | padding-bottom: 0; 236 | overflow: visible !important; } 237 | .navbar-nav { 238 | float: left !important; 239 | margin: 0; } 240 | .navbar-right { 241 | float: right !important; 242 | margin: 0; } 243 | .navbar-nav>li { 244 | float: left; } 245 | .navbar-nav>li>a { 246 | height: 50px; 247 | padding-top: 10px; 248 | line-height: 30px; 249 | padding-bottom: 10px; } 250 | .navbar-right>li>a { 251 | padding-top: 16px; } 252 | } 253 | 254 | // readable phone splash 255 | 256 | @media (min-width: 600px) { 257 | .navbar a { font-size: 25px; } 258 | } 259 | 260 | 261 | // making custom navigation bar 262 | 263 | #sitenav 264 | z-index 1 265 | background white 266 | max-height 50px 267 | position fixed 268 | height 50px 269 | width 100% 270 | left 0px 271 | top 0px 272 | #sitenav > div 273 | display block 274 | height 50px 275 | a.sitelink 276 | border-radius(0.2em) 277 | font-family monospace 278 | display inline-block 279 | margin-right 1em 280 | v-center(12px) 281 | font-size 20px 282 | color logored 283 | a.sitelink.grn.login 284 | margin-right -0.5em 285 | a.sitelink:hover 286 | background logored 287 | color white 288 | #siteleft 289 | padding-left 1em 290 | float left 291 | #siteright 292 | padding-right 1.5em 293 | float right 294 | #siteimg 295 | max-height 50px 296 | padding 7px 297 | float left 298 | -------------------------------------------------------------------------------- /client/render/raw.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 97 | -------------------------------------------------------------------------------- /client/render/raw.js: -------------------------------------------------------------------------------- 1 | // iframe helper - load editor content 2 | 3 | const prof = GitSync.prof; 4 | const clean = GitSync.sanitizeStringQuotes; 5 | const getTag = GitSync.grabTagContentsToRender; 6 | const findFile = GitSync.findFileFromExt; 7 | 8 | 9 | Template.raw.helpers({ 10 | 11 | getUser() { // return id of current user 12 | if (Meteor.user()) 13 | return Meteor.userId(); 14 | }, 15 | 16 | getRepo() { // return id of project repo 17 | if (Meteor.user()) 18 | return prof().repo; 19 | }, 20 | 21 | getHead() { // parse head of html file 22 | const full = findFile("html"); 23 | if (full) 24 | return getTag(full, "head"); 25 | }, 26 | 27 | getBody() { // parse body of file 28 | const full = findFile("html"); 29 | if (full) 30 | return getTag(full, "body"); 31 | }, 32 | 33 | getCSS() { 34 | const css = findFile("css"); 35 | if (css) 36 | return css.content; 37 | }, 38 | 39 | getJS() { 40 | const js = findFile("js"); 41 | if (js) 42 | return js.content; 43 | }, 44 | 45 | htmlString() { // for attaching content to issue 46 | const full = findFile("html"); 47 | if (full) 48 | return clean(full.content); 49 | }, 50 | 51 | cssString() { 52 | const css = findFile("css"); 53 | if (css) 54 | return clean(css.content); 55 | }, 56 | 57 | jsString() { 58 | const js = findFile("js"); 59 | if (js) 60 | return clean(js.content); 61 | }, 62 | 63 | }); 64 | 65 | -------------------------------------------------------------------------------- /client/render/render.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 34 | 35 | -------------------------------------------------------------------------------- /client/save/save.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 29 | 30 | 67 | 68 | 69 | 70 | 86 | 87 | 124 | 125 | 126 | 127 | 140 | 141 | 157 | 158 | -------------------------------------------------------------------------------- /client/save/save.js: -------------------------------------------------------------------------------- 1 | // git things - version control, importing code 2 | 3 | const difflib = Difflib.lib; 4 | const diffview = Difflib.view; 5 | 6 | const prof = GitSync.prof; 7 | const ufids = GitSync.ufids; 8 | const ufiles = GitSync.userfiles; 9 | const clean = GitSync.sanitizeDiffs; 10 | const focusForm = GitSync.focusForm; 11 | const labelLineNumbers = GitSync.labelLineNumbers; 12 | 13 | Template.commitPanel.helpers({ 14 | 15 | branch() { 16 | return prof().repoBranch; 17 | }, 18 | 19 | committing() { 20 | return Session.equals("focusPane", "committer"); 21 | }, 22 | 23 | changes() { 24 | return !GitSync.changes() 25 | }, 26 | 27 | }); 28 | 29 | Template.commitPanel.events({ 30 | 31 | "click .newcommit"(e) { 32 | e.preventDefault(); 33 | FirepadAPI.getAllText(ufids(), (id, txt) => { 34 | Meteor.call("updateFile", id, txt); }); 35 | Session.set("focusPane", "committer"); 36 | focusForm("#commitMsg"); 37 | }, 38 | 39 | "submit .committer"(e) { 40 | e.preventDefault(); 41 | $(e.target).blur(); 42 | const msg = $("#commitMsg")[0].value; 43 | if (msg == null || msg == "") return false; // dont allow empty commit msgs 44 | Session.set("committing", null); 45 | Session.set("focusPane", null); 46 | Meteor.call("newCommit", msg); 47 | }, 48 | 49 | "click .cancelCommit"(e) { 50 | e.preventDefault(); 51 | Session.set("focusPane", null); 52 | }, 53 | 54 | "click .reload"(e) { // pull in latest version of buffers 55 | e.preventDefault(); 56 | FirepadAPI.getAllText(ufids(), (id, txt) => { 57 | Meteor.call("updateFile", id, txt); }); 58 | }, 59 | 60 | "click .loadhead"(e) { // load head of branch into SJS 61 | e.preventDefault(); 62 | let trulyLoad = confirm("This will overwrite any uncommitted changes. Proceed?"); 63 | if (trulyLoad) { 64 | Session.set("loadingRepo", true); 65 | Meteor.call("loadHead", prof().repoBranch, function () { 66 | FirepadAPI.setAllText(function onDone() { 67 | Session.set("loadingRepo", false); 68 | }); 69 | }); 70 | } 71 | }, 72 | 73 | }); 74 | 75 | Template.history.helpers({ // sort the commits by time 76 | 77 | commits() { 78 | return Commits.find({}, {sort: {"commit.commit.committer.date": -1}} ); 79 | }, 80 | 81 | commitCount() { 82 | return Commits.find({}).count(); 83 | }, 84 | 85 | }); 86 | 87 | Template.history.events({ 88 | 89 | "click .reload"(e) { // pull in latest commits from gh 90 | e.preventDefault(); 91 | Meteor.call("initCommits"); 92 | }, 93 | 94 | }); 95 | 96 | Template.commit.helpers({ 97 | 98 | current() { 99 | return Session.equals("focusPane", this._id); 100 | }, 101 | 102 | mine() { 103 | const myprof = prof(); 104 | if (myprof && this.commit && this.commit.author) 105 | return (myprof.login === this.commit.author.login) 106 | }, 107 | 108 | }); 109 | 110 | Template.commit.events({ 111 | 112 | "click .commit"(e) { 113 | if (Session.equals("focusPane", this._id)) 114 | Session.set("focusPane", null); 115 | else 116 | Session.set("focusPane", this._id); 117 | }, 118 | 119 | "click .loadcommit"(e) { 120 | const trulyLoad = confirm("This will overwrite any uncommitted changes. Proceed?"); 121 | if (trulyLoad) { 122 | Session.set("focusPane", null); 123 | Meteor.call("loadCommit", this.commit.sha); 124 | FirepadAPI.setAllText(); 125 | } 126 | }, 127 | 128 | }); 129 | 130 | 131 | 132 | 133 | // RENDERING DIFFS 134 | 135 | Template.statusPanel.helpers({ 136 | 137 | changes() { // return true if any diffs exist. 138 | return GitSync.changes(); 139 | }, 140 | 141 | diffs() { // using jsdiff, return a diff on each file 142 | return ufiles().map(function checkDiff(file){ // content v cache 143 | if(file.content !== file.cache) // return the different file 144 | return { 145 | id: file._id, 146 | title: file.title, 147 | content: file.content, 148 | cache: file.cache 149 | }; 150 | }).filter(function removeNull(diff){ 151 | return diff != undefined; 152 | }); 153 | }, 154 | 155 | }); 156 | 157 | Template.diff.helpers({ 158 | 159 | lines() { 160 | if (this.content === this.cache) return; // nodiff 161 | 162 | const base = difflib.stringAsLines( this.cache ); 163 | const newtxt = difflib.stringAsLines( this.content ); 164 | const sm = new difflib.SequenceMatcher(base, newtxt); 165 | const opcodes = sm.get_opcodes(); 166 | const context = 1; // relevant rows 167 | 168 | const codeview = diffview.buildView({ 169 | opcodes, 170 | baseTextLines: base, 171 | newTextLines: newtxt, 172 | baseTextName: "base", 173 | newTextName: "new", 174 | contextSize: context, 175 | viewType: 1, // 0 for side by side, 1 for inline diff 176 | }); 177 | 178 | return codeview.map(function parse(x){ 179 | 180 | // parsing out the old and new line numbers 181 | const linedata = {}; 182 | let oldnum, newnum; 183 | const numinfo = x.getElementsByTagName("th"); 184 | if (numinfo[0] == undefined || numinfo[1] == undefined) 185 | return; // for some reason no line numbers??? 186 | 187 | // parsing out the table data (edit/delete) 188 | let allinfo, info, status, content; 189 | allinfo = x.getElementsByTagName("td"); 190 | if (allinfo[0] == undefined) 191 | return; // empty tags??? 192 | 193 | // building up the line data information 194 | linedata.oldnum = numinfo[0].innerHTML; 195 | linedata.newnum = numinfo[1].innerHTML; 196 | linedata.status = allinfo[0].getAttribute("class"); 197 | linedata.content = clean(allinfo[0].innerHTML); 198 | return linedata; 199 | 200 | }).filter(function denull(l){ // remove any empty rows 201 | return l != undefined; 202 | }); 203 | }, 204 | 205 | }); 206 | 207 | Template.diff.events({ 208 | 209 | "click .reset"(e) { 210 | let id = this.id 211 | const trulyReset = confirm("This will reset this file back to the last commit. Proceed?"); 212 | if (trulyReset) { // TODO fix this so it doesn't use an api call. info is stored locally!!! 213 | Session.set("loadingRepo", true); 214 | Meteor.call("resetFile", id, function(){ 215 | FirepadAPI.setText(id, function(){ 216 | Session.set("loadingRepo", false); 217 | }) 218 | }); 219 | } 220 | } 221 | 222 | }); 223 | 224 | Template.diffline.helpers({ 225 | 226 | content() { return this.content; }, 227 | skipped() { return this.status == "skip" }, 228 | equal() { return this.status == "equal" }, 229 | inserted() { return this.status == "insert" }, 230 | deleted() { return this.status == "delete" }, 231 | 232 | newnum() { 233 | const num = this.newnum; 234 | if (num > 0) 235 | return num 236 | else 237 | return "-" 238 | }, 239 | 240 | oldnum() { 241 | const num = this.newnum; 242 | if (num > 0) 243 | return num 244 | else 245 | return "-" 246 | }, 247 | 248 | }); 249 | 250 | -------------------------------------------------------------------------------- /client/test/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 56 | 57 | 58 | 111 | 112 | 113 | 129 | 130 | 155 | 156 | 157 | 158 | 159 | 160 | 180 | 181 | 203 | 204 | 207 | 208 | 209 | 210 | 211 | 212 | 215 | 216 | 219 | 220 | 303 | -------------------------------------------------------------------------------- /client/test/test.js: -------------------------------------------------------------------------------- 1 | // testing page management 2 | 3 | const ufids = GitSync.ufids; 4 | 5 | Template.testfile.helpers({ 6 | files() { 7 | return Files.find({}, {sort: {"title": 1}} ) 8 | }, 9 | 10 | current() { 11 | return Session.equals("testFile", this._id); 12 | }, 13 | }); 14 | 15 | 16 | Template.testfile.events({ 17 | 18 | "click .file"() { 19 | Session.set("testFile", this._id); 20 | Session.set("focusPane", null); 21 | }, 22 | 23 | }); 24 | 25 | 26 | Template.testviz.helpers({ 27 | enabled() { 28 | return Session.get("testViz"); 29 | }, 30 | 31 | file() { 32 | return !Session.equals("testFile", null) 33 | }, 34 | 35 | mode() { 36 | return GitSync.findFileMode(Session.get("testFile")) 37 | }, 38 | 39 | lang() { 40 | const mode = GitSync.findFileMode(Session.get("testFile")); 41 | return GitSync.tutorMap[mode]; 42 | }, 43 | 44 | target() { 45 | return Session.equals("focusPane", "target"); 46 | }, 47 | 48 | testcode() { 49 | const file = Files.findOne(Session.get("testFile")); 50 | if (file) 51 | return encodeURIComponent(file.content); 52 | }, 53 | 54 | title() { 55 | const file = Files.findOne(Session.get("testFile")); 56 | if (file) 57 | return file.title; 58 | }, 59 | }); 60 | 61 | 62 | Template.testviz.events({ 63 | "load #testviz"() { 64 | id = "#testviz" 65 | $(id).load(function() { 66 | $(this).height( 600 ); 67 | }); 68 | }, 69 | 70 | "click .toggle"(e) { 71 | e.preventDefault(); 72 | Session.set("testViz", !Session.get("testViz") ); 73 | }, 74 | 75 | "click .target"(e) { 76 | e.preventDefault(); 77 | if ( Session.equals("focusPane", "target") ) 78 | Session.set("focusPane", null); 79 | else 80 | Session.set("focusPane", "target"); 81 | }, 82 | 83 | "click .reload"(e) { 84 | e.preventDefault(); 85 | FirepadAPI.getAllText(ufids(), (id, txt) => { 86 | Meteor.call("updateFile", id, txt); }); 87 | Session.set("testViz", !Session.get("testViz") ); 88 | setTimeout(() => { 89 | Session.set("testViz", !Session.get("testViz") ); 90 | }, 100); 91 | }, 92 | }); 93 | 94 | 95 | 96 | // interactive testing 97 | 98 | Template.testint.helpers({ 99 | enabled() { 100 | return Session.get("testInt"); 101 | }, 102 | 103 | file() { 104 | return !Session.equals("testFile", null) 105 | }, 106 | 107 | mode() { 108 | return GitSync.findFileMode(Session.get("testFile")) 109 | }, 110 | 111 | python() { 112 | const mode = GitSync.findFileMode(Session.get("testFile")); 113 | return GitSync.interactMap[mode] == "python"; 114 | }, 115 | 116 | ruby() { 117 | const mode = GitSync.findFileMode(Session.get("testFile")); 118 | return GitSync.interactMap[mode] == "ruby"; 119 | }, 120 | 121 | js() { 122 | const mode = GitSync.findFileMode(Session.get("testFile")); 123 | return GitSync.interactMap[mode] == "javascript"; 124 | }, 125 | 126 | unsupported() { 127 | const mode = GitSync.findFileMode(Session.get("testFile")); 128 | return ! GitSync.interactMap[mode]; // false if string 129 | }, 130 | 131 | target() { 132 | return Session.equals("focusPane", "target"); 133 | }, 134 | 135 | testcode() { 136 | const file = Files.findOne(Session.get("testFile")); 137 | if (file) return encodeURIComponent(file.content); 138 | }, 139 | 140 | title() { 141 | const file = Files.findOne(Session.get("testFile")); 142 | if (file) return file.title; 143 | }, 144 | }); 145 | 146 | 147 | Template.testint.events({ 148 | "load #testint"() { 149 | $(".resize").resizable({ handles: "s", helper: "ui-resizable-helper" }); 150 | }, 151 | 152 | "click .toggle"(e) { 153 | e.preventDefault(); 154 | Session.set("testInt", !Session.get("testInt") ); 155 | }, 156 | 157 | "click .target"(e) { 158 | e.preventDefault(); 159 | if ( Session.equals("focusPane", "target") ) 160 | Session.set("focusPane", null); 161 | else 162 | Session.set("focusPane", "target"); 163 | }, 164 | 165 | "click .reload"(e) { 166 | e.preventDefault(); 167 | FirepadAPI.getAllText(ufids(), (id, txt) => { 168 | Meteor.call("updateFile", id, txt); }); 169 | Session.set("testInt", !Session.get("testInt") ); 170 | setTimeout(() => { 171 | Session.set("testInt", !Session.get("testInt") ); 172 | }, 100); 173 | }, 174 | }); 175 | 176 | 177 | Template.interactJs.helpers({ 178 | testcode() { 179 | const file = Files.findOne(Session.get("testFile")); 180 | if (file) return (file.content); 181 | }, 182 | }) 183 | 184 | 185 | 186 | 187 | Template.testweb.helpers({ 188 | enabled() { 189 | return Session.get("testWeb"); 190 | }, 191 | }); 192 | 193 | Template.testweb.events({ 194 | "load #testweb"() { 195 | id = "#testweb" 196 | 197 | GitSync.focusForm(id) 198 | setInterval(function() { 199 | try { 200 | var h = $(id).contents().find("iframe").contents().height() 201 | $(id).height(h); 202 | } catch (e) {} 203 | }, 100); 204 | }, 205 | 206 | "click .toggle"(e) { 207 | e.preventDefault(); 208 | Session.set("testWeb", !Session.get("testWeb") ); 209 | }, 210 | 211 | "click .reload"(e) { 212 | e.preventDefault(); 213 | FirepadAPI.getAllText(ufids(), (id, txt) => { 214 | Meteor.call("updateFile", id, txt); }); 215 | $("#testweb")[0].contentWindow.location.reload(true) 216 | }, 217 | }); 218 | 219 | 220 | 221 | // github issue event integration 222 | 223 | Template.issues.helpers({ 224 | issues() { // sort and return issues for this repo 225 | return Issues.find({}, {sort: {"issue.updated_at": -1}}); 226 | }, 227 | 228 | issueCount() { // return amount of open issues 229 | return Issues.find({}).count(); 230 | }, 231 | }); 232 | 233 | Template.issues.events({ 234 | "click .reload"(e) { // update the issues for this repo 235 | e.preventDefault(); 236 | Meteor.call("initIssues"); 237 | } 238 | }); 239 | 240 | 241 | 242 | // individual issue helpers 243 | 244 | Template.issue.helpers({ 245 | current() { 246 | return Session.equals("focusPane", this._id); 247 | }, 248 | 249 | screen() { // return an issue screenshot 250 | let screen; 251 | if (this.feedback) 252 | screen = Screens.findOne(this.feedback.imglink); 253 | if (screen) 254 | return screen.img; 255 | }, 256 | 257 | labels() { 258 | if (this.issue) 259 | return this.issue.labels; 260 | }, 261 | }); 262 | 263 | Template.issue.events({ 264 | "click .issue"(e) { // click to focus issue, again to reset 265 | if ( Session.equals("focusPane", this._id) ) 266 | Session.set("focusPane", null); 267 | else 268 | Session.set("focusPane", this._id); 269 | }, 270 | 271 | "click .closeissue"(e) { // click to close a given issue 272 | Meteor.call("closeIssue", this); 273 | }, 274 | }); 275 | 276 | 277 | // resize in a timely manner 278 | 279 | Template.interactJs.onRendered(() => { 280 | id = "#interactJs" 281 | 282 | $(id).load(function() { 283 | $(this).height( $(this).contents().find("html").height() ); 284 | }); 285 | 286 | setInterval(function() { 287 | $(id).height( $(id).contents().find("html").height() ); 288 | }, 100); 289 | }) 290 | -------------------------------------------------------------------------------- /history.md: -------------------------------------------------------------------------------- 1 | TODOS 2 | ===== 3 | 4 | ## todoist dump 5 | 6 | including current commit data 7 | filter files to those in current commit 8 | adding debugger to interact panel 9 | when you set repo, current document should become null 10 | adding loading icon for when checking out a commirt 11 | optimize reset button to just set current version to the cached version 12 | passing in complete callback on done 13 | before files ready, set up the loading screen 14 | change loadingrepo to just loading session var 15 | make feed be plus or minus dependent on whether is expanded or not... 16 | make left bar less wide 17 | show side by side editing 18 | use markdown syntax to embed image inline 19 | Tagging visual updates of issues with code (save that as screenshot) 20 | break apart the visualization into two separate parts with css (x-domain issues) 21 | Provided by Todoist.com 1 of 1 11/24/17, 1:18 PM 22 | 23 | # older todos 24 | 25 | better notifications of who is working on project, what file is open in their 26 | buffer and what branch they are on. 27 | 28 | filter sharejs loading to type == file 29 | 30 | collaboration idea: in the users tab, add ability to add a copilot by their 31 | username on github, as well as the ability to revoke if you are true user great 32 | BC they dont have to be github collab to edit it!!! adding revokable users to a 33 | repo?? (unsure if this can be done with current api) 34 | 35 | when loading a new repo in, display that in the files, so user knows, 36 | then reset session variable after successful load (or error) in cb() 37 | this takes some time, and the buffers are just empty until loading 38 | 39 | when you make a new file, list all the commit ids that it appears in so you can 40 | filter which files should show up based on the current commit 41 | 42 | filtering which files to show based on the current commit, only enabling one 43 | commit per repoBranch to be active at a time, so therefore must be attached to 44 | a repobranch rather than a user. however, each branch is only attached to user, 45 | so instead we can attach the active commit for each branch to a repo! so in the 46 | repos field, there will be a branches dict with a keymap from branch name to 47 | sha. then in the files we can add an array of commits in which they appear so 48 | we can filter out files which do not appear in certain commits. 49 | 50 | gahhhhh really need to switch to using node-git instead of doing so many api calls. 51 | 52 | 53 | ## UI / UX 54 | 55 | adjust the js debugger depending on the screen size 56 | marking a release on github plz 57 | expose editor configuration to user 58 | add a footer in for spacing in main 59 | loading bar for the commit progress 60 | icons may seem to do action - remove icon 61 | normalize role selection (match others) 62 | rendering .md as a link in feed 63 | setting up different roles - junior prog 64 | describe roles differences much better 65 | console.error() on loadrepo?? 66 | 67 | 68 | 69 | ## SERVER 70 | 71 | \_pick your data as to not bloat the database 72 | autoload repos once after creating an account 73 | only request commits after current 74 | install loglevel meteor 75 | rendering local images in a view? 76 | repo has label-created field, only call once 77 | more testing with someone else 78 | handle users that do not have any repos 79 | rendering an arbitrary commit 80 | deleting / renaming files with github 81 | if owner, linking to the collaborators page 82 | implementing a collapseable folder structure 83 | load file from github only on click?? this will reduce api calls 84 | get current commit sha -> tree sha -> blob -> load into sharejs doc 85 | for handling larger projects without destroying github api: 86 | possible to store versions of each file?? 87 | 88 | 89 | ## STUDY 90 | 91 | "what makes for the most innovative pair and multi-user coding environment" 92 | what are interesting things that people could run in an hour 93 | beyond study - lickert scale study questions 94 | github vs git, able to collaborate, but not 95 | (within subjects takes care of it) 96 | auxiliray - coding spectator to jump in 97 | live webcasting of them coding, people can help 98 | GitSync for strangers (pull requests vs GitSync) 99 | one pilot and many coding helpers 100 | 101 | 102 | 103 | ## PAPER 104 | 105 | copilot nosiness - editing code, passivity? 106 | user study measurements - what metrics to evaluate? 107 | case study from Mythical Man Month of surgeon + multiple assistants 108 | a distributed task 1 pilot, 4 copilots 109 | video or talk embedding - collab github education? 110 | creating an issue on this repo, could be helpful to PROF 111 | github as an education platform, GitSync as a even *more* collaborative platform? 112 | a teacher can have lecture code stored in the repo, and then walk through bit 113 | by bit (eg commits), even if not runnable in the browser/cloud form controls 114 | make note of andy, talking about debugging webapps, related to work that bryan 115 | burg did at uw while he was a student of andys. technical hci work, good to 116 | cite the uist work, very benficial for the apps upcoming very soon 117 | summary of what you did, and a copy of the upd to date resume 118 | 119 | by creating a gh-pages branch, you can actually host the content that you make 120 | from a specific readme - this is cool, because it lets you export the code from 121 | the renderer to an actual live webpage. some dependencies may break however. 122 | 123 | Yep! GitSync is very relevant nowadays since more and more people are working 124 | remotely as software developers. Remote and distributed development teams are 125 | more of a norm now, so people need better situational awareness and pair 126 | programming tools for this new workflow. That's a great thing to include in an 127 | intro for a paper and your thesis. 128 | 129 | #### FUTURE WORK 130 | 131 | Let the pair switch off whenever they want if one person is getting tired or 132 | stuck or wants a chill break to be hanging out more passively in the background 133 | maybe include an automated metric on how 'in flow' the person is, suggest a 134 | change if it drops below a certain point 135 | 136 | 137 | 138 | ## NOTES / ERRORS 139 | 140 | - an untitled file sometimes gets generated when switching repos/branches 141 | - after longtime of unwatching: Uncaught SyntaxError: Unexpected token Y start 142 | - file contents while renaming, many errors 143 | - when email is set to private, label is not shown 144 | 145 | ## HISTORY 146 | 147 | ## vitchyr git-sync feedback 148 | 149 | wanted to know more about why he would use it 150 | initial interface was overly complex, rip out file opening notifications 151 | potentially collapsing parts of the ui initially, allow user to code 152 | making a getting started ui to show around different features 153 | loading screen for when importing or switching repos 154 | about panel makes it look it is about them rather than tool 155 | updating repo pane to includ3e git status data 156 | making the backend FS depend on git rather than github 157 | splitting the visualizer in the iframe so they are vertically stacked 158 | also still need to update the version control 159 | give more explanation to people before showing 160 | 161 | ## from study evaluation 162 | 163 | better indicators of what the other person is working on 164 | database version fails to stay updated for long 165 | minified version of control bar is broken 166 | ability to see what file they have open 167 | provide a link back to the code 168 | make owners pilots by default 169 | hiding git information 170 | 171 | ## aaron comments 172 | 173 | - explicity select html/css/js 174 | - create user should route to home 175 | - inviting users to edit w/o github collaborator 176 | 177 | ## abdullah feedback 178 | 179 | - request a link for collaborators 180 | - smaller link for generation 181 | 182 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "git-sync", 3 | "version": "1.0.0", 4 | "description": "[git-sync](http://git-sync.com) ==================================", 5 | "main": "index.js", 6 | "dependencies": { 7 | "babel-runtime": "^6.18.0", 8 | "bcrypt": "^0.8.7", 9 | "spacejam": "^1.6.1" 10 | }, 11 | "devDependencies": {}, 12 | "scripts": { 13 | "test": "echo \"Error: no test specified\" && exit 1" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/jeremywrnr/git-sync.git" 18 | }, 19 | "author": "", 20 | "license": "ISC", 21 | "bugs": { 22 | "url": "https://github.com/jeremywrnr/git-sync/issues" 23 | }, 24 | "homepage": "https://github.com/jeremywrnr/git-sync#readme" 25 | } 26 | -------------------------------------------------------------------------------- /packages/difflib/difflib-tests.js: -------------------------------------------------------------------------------- 1 | // difflib tests 2 | 3 | Tinytest.add("difflib", function (test) { 4 | //test.equal(x, "x") 5 | //test.equal(y, link(site)) 6 | //test.equal(z, site1 + link(site2)) 7 | }); 8 | 9 | -------------------------------------------------------------------------------- /packages/difflib/difflib.js: -------------------------------------------------------------------------------- 1 | /** Builds and returns a visual diff view. The single parameter, `params', 2 | * should contain the following values: - baseTextLines: the array of strings 3 | * that was used as the base text input to SequenceMatcher - newTextLines: the 4 | * array of strings that was used as the new text input to SequenceMatcher - 5 | * opcodes: the array of arrays returned by SequenceMatcher.get_opcodes() - 6 | * baseTextName: the title to be displayed above the base text listing in 7 | * the diff view; defaults to "Base Text" - newTextName: the title to be 8 | * displayed above the new text listing in the diff view; defaults to 9 | * "New Text" - contextSize: the number of lines of context to show around 10 | * differences; by default, all lines are shown - viewType: if 0, a 11 | * side-by-side diff view is generated * (default); if 1, an inline diff 12 | * view is generated */ 13 | 14 | 15 | // prep export 16 | Difflib = {}; 17 | 18 | 19 | var diffview = { 20 | buildView: function (params) { 21 | var baseTextLines = params.baseTextLines; 22 | var newTextLines = params.newTextLines; 23 | var opcodes = params.opcodes; 24 | var baseTextName = params.baseTextName ? params.baseTextName : "Base Text"; 25 | var newTextName = params.newTextName ? params.newTextName : "New Text"; 26 | var contextSize = params.contextSize; 27 | var inline = (params.viewType === 0 || params.viewType === 1) ? params.viewType : 0; 28 | 29 | if (baseTextLines === null) 30 | throw "Cannot build diff view; baseTextLines is not defined."; 31 | if (newTextLines === null) 32 | throw "Cannot build diff view; newTextLines is not defined."; 33 | if (!opcodes) 34 | throw "Canno build diff view; opcodes is not defined."; 35 | 36 | function celt (name, clazz) { 37 | var e = document.createElement(name); 38 | e.className = clazz; 39 | return e; 40 | } 41 | 42 | function telt (name, text) { 43 | var e = document.createElement(name); 44 | e.appendChild(document.createTextNode(text)); 45 | return e; 46 | } 47 | 48 | function ctelt (name, clazz, text) { 49 | var e = document.createElement(name); 50 | e.className = clazz; 51 | e.appendChild(document.createTextNode(text)); 52 | return e; 53 | } 54 | 55 | var tdata = document.createElement("thead"); 56 | var node = document.createElement("tr"); 57 | tdata.appendChild(node); 58 | if (inline) { 59 | node.appendChild(document.createElement("th")); 60 | node.appendChild(document.createElement("th")); 61 | node.appendChild(ctelt("th", "texttitle", baseTextName + " vs. " + newTextName)); 62 | } else { 63 | node.appendChild(document.createElement("th")); 64 | node.appendChild(ctelt("th", "texttitle", baseTextName)); 65 | node.appendChild(document.createElement("th")); 66 | node.appendChild(ctelt("th", "texttitle", newTextName)); 67 | } 68 | tdata = [tdata]; 69 | 70 | var rows = []; 71 | var node2; 72 | 73 | /** 74 | Adds two cells to the given row; if the given row corresponds to a real 75 | line number (based on the line index tidx and the endpoint of the 76 | range in question tend), then the cells will contain the line number 77 | and the line of text from textLines at position tidx (with the class of 78 | the second cell set to the name of the change represented), and tidx + 1 will 79 | be returned. Otherwise, tidx is returned, and two empty cells are added 80 | to the given row. 81 | */ 82 | function addCells (row, tidx, tend, textLines, change) { 83 | if (tidx < tend) { 84 | row.appendChild(telt("th", (tidx + 1).toString())); 85 | row.appendChild(ctelt("td", change, textLines[tidx].replace(/\t/g, "\u00a0\u00a0\u00a0\u00a0"))); 86 | return tidx + 1; 87 | } else { 88 | row.appendChild(document.createElement("th")); 89 | row.appendChild(celt("td", "empty")); 90 | return tidx; 91 | } 92 | } 93 | 94 | function addCellsInline (row, tidx, tidx2, textLines, change) { 95 | row.appendChild(telt("th", tidx === null ? "" : (tidx + 1).toString())); 96 | row.appendChild(telt("th", tidx2 === null ? "" : (tidx2 + 1).toString())); 97 | row.appendChild(ctelt("td", change, textLines[tidx != null ? tidx : tidx2].replace(/\t/g, "\u00a0\u00a0\u00a0\u00a0"))); 98 | } 99 | 100 | for (var idx = 0; idx < opcodes.length; idx++) { 101 | code = opcodes[idx]; 102 | change = code[0]; 103 | var b = code[1]; 104 | var be = code[2]; 105 | var n = code[3]; 106 | var ne = code[4]; 107 | var rowcnt = Math.max(be - b, ne - n); 108 | var toprows = []; 109 | var botrows = []; 110 | for (var i = 0; i < rowcnt; i++) { 111 | // jump ahead if we've alredy provided leading context or if this is the first range 112 | if (contextSize && opcodes.length > 1 && ((idx > 0 && i === contextSize) || (idx === 0 && i === 0)) && change==="equal") { 113 | var jump = rowcnt - ((idx === 0 ? 1 : 2) * contextSize); 114 | if (jump > 1) { 115 | toprows.push(node = document.createElement("tr")); 116 | 117 | b += jump; 118 | n += jump; 119 | i += jump - 1; 120 | node.appendChild(telt("th", "...")); 121 | if (!inline) node.appendChild(ctelt("td", "skip", "")); 122 | node.appendChild(telt("th", "...")); 123 | node.appendChild(ctelt("td", "skip", "")); 124 | 125 | // skip last lines if they're all equal 126 | if (idx + 1 === opcodes.length) { 127 | break; 128 | } else { 129 | continue; 130 | } 131 | } 132 | } 133 | 134 | toprows.push(node = document.createElement("tr")); 135 | if (inline) { 136 | if (change === "insert") { 137 | addCellsInline(node, null, n++, newTextLines, change); 138 | } else if (change === "replace") { 139 | botrows.push(node2 = document.createElement("tr")); 140 | if (b < be) addCellsInline(node, b++, null, baseTextLines, "delete"); 141 | if (n < ne) addCellsInline(node2, null, n++, newTextLines, "insert"); 142 | } else if (change === "delete") { 143 | addCellsInline(node, b++, null, baseTextLines, change); 144 | } else { 145 | // equal 146 | addCellsInline(node, b++, n++, baseTextLines, change); 147 | } 148 | } else { 149 | b = addCells(node, b, be, baseTextLines, change); 150 | n = addCells(node, n, ne, newTextLines, change); 151 | } 152 | } 153 | 154 | for (var i = 0; i < toprows.length; i++) rows.push(toprows[i]); 155 | for (var i = 0; i < botrows.length; i++) rows.push(botrows[i]); 156 | } 157 | 158 | //tdata.push(node = document.createElement("tbody")); 159 | //for (var idx in rows) rows.hasOwnProperty(idx) && node.appendChild(rows[idx]); 160 | 161 | //node = celt("table", "diff" + (inline ? " inlinediff" : "")); 162 | //for (var idx in tdata) tdata.hasOwnProperty(idx) && node.appendChild(tdata[idx]); 163 | // 164 | return rows; 165 | } 166 | }; 167 | 168 | var __whitespace = {" ":true, "\t":true, "\n":true, "\f":true, "\r":true}; 169 | 170 | 171 | var difflib = { 172 | defaultJunkFunction: function (c) { 173 | return __whitespace.hasOwnProperty(c); 174 | }, 175 | 176 | stripLinebreaks: function (str) { return str.replace(/^[\n\r]*|[\n\r]*$/g, ""); }, 177 | 178 | stringAsLines: function (str) { 179 | var lfpos = str.indexOf("\n"); 180 | var crpos = str.indexOf("\r"); 181 | var linebreak = ((lfpos > -1 && crpos > -1) || crpos < 0) ? "\n" : "\r"; 182 | 183 | var lines = str.split(linebreak); 184 | for (var i = 0; i < lines.length; i++) { 185 | lines[i] = difflib.stripLinebreaks(lines[i]); 186 | } 187 | 188 | return lines; 189 | }, 190 | 191 | // iteration-based reduce implementation 192 | __reduce: function (func, list, initial) { 193 | if (initial != null) { 194 | var value = initial; 195 | var idx = 0; 196 | } else if (list) { 197 | var value = list[0]; 198 | var idx = 1; 199 | } else { 200 | return null; 201 | } 202 | 203 | for (; idx < list.length; idx++) { 204 | value = func(value, list[idx]); 205 | } 206 | 207 | return value; 208 | }, 209 | 210 | // comparison function for sorting lists of numeric tuples 211 | __ntuplecomp: function (a, b) { 212 | var mlen = Math.max(a.length, b.length); 213 | for (var i = 0; i < mlen; i++) { 214 | if (a[i] < b[i]) return -1; 215 | if (a[i] > b[i]) return 1; 216 | } 217 | 218 | return a.length == b.length ? 0 : (a.length < b.length ? -1 : 1); 219 | }, 220 | 221 | __calculate_ratio: function (matches, length) { 222 | return length ? 2.0 * matches / length : 1.0; 223 | }, 224 | 225 | // returns a function that returns true if a key passed to the returned function 226 | // is in the dict (js object) provided to this function; replaces being able to 227 | // carry around dict.has_key in python... 228 | __isindict: function (dict) { 229 | return function (key) { return dict.hasOwnProperty(key); }; 230 | }, 231 | 232 | // replacement for python's dict.get function -- need easy default values 233 | __dictget: function (dict, key, defaultValue) { 234 | return dict.hasOwnProperty(key) ? dict[key] : defaultValue; 235 | }, 236 | 237 | SequenceMatcher: function (a, b, isjunk) { 238 | this.set_seqs = function (a, b) { 239 | this.set_seq1(a); 240 | this.set_seq2(b); 241 | } 242 | 243 | this.set_seq1 = function (a) { 244 | if (a == this.a) return; 245 | this.a = a; 246 | this.matching_blocks = this.opcodes = null; 247 | } 248 | 249 | this.set_seq2 = function (b) { 250 | if (b == this.b) return; 251 | this.b = b; 252 | this.matching_blocks = this.opcodes = this.fullbcount = null; 253 | this.__chain_b(); 254 | } 255 | 256 | this.__chain_b = function () { 257 | var b = this.b; 258 | var n = b.length; 259 | var b2j = this.b2j = {}; 260 | var populardict = {}; 261 | for (var i = 0; i < b.length; i++) { 262 | var elt = b[i]; 263 | if (b2j.hasOwnProperty(elt)) { 264 | var indices = b2j[elt]; 265 | if (n >= 200 && indices.length * 100 > n) { 266 | populardict[elt] = 1; 267 | delete b2j[elt]; 268 | } else { 269 | indices.push(i); 270 | } 271 | } else { 272 | b2j[elt] = [i]; 273 | } 274 | } 275 | 276 | for (var elt in populardict) { 277 | if (populardict.hasOwnProperty(elt)) { 278 | delete b2j[elt]; 279 | } 280 | } 281 | 282 | var isjunk = this.isjunk; 283 | var junkdict = {}; 284 | if (isjunk) { 285 | for (var elt in populardict) { 286 | if (populardict.hasOwnProperty(elt) && isjunk(elt)) { 287 | junkdict[elt] = 1; 288 | delete populardict[elt]; 289 | } 290 | } 291 | for (var elt in b2j) { 292 | if (b2j.hasOwnProperty(elt) && isjunk(elt)) { 293 | junkdict[elt] = 1; 294 | delete b2j[elt]; 295 | } 296 | } 297 | } 298 | 299 | this.isbjunk = difflib.__isindict(junkdict); 300 | this.isbpopular = difflib.__isindict(populardict); 301 | } 302 | 303 | this.find_longest_match = function (alo, ahi, blo, bhi) { 304 | var a = this.a; 305 | var b = this.b; 306 | var b2j = this.b2j; 307 | var isbjunk = this.isbjunk; 308 | var besti = alo; 309 | var bestj = blo; 310 | var bestsize = 0; 311 | var j = null; 312 | var k; 313 | 314 | var j2len = {}; 315 | var nothing = []; 316 | for (var i = alo; i < ahi; i++) { 317 | var newj2len = {}; 318 | var jdict = difflib.__dictget(b2j, a[i], nothing); 319 | for (var jkey in jdict) { 320 | if (jdict.hasOwnProperty(jkey)) { 321 | j = jdict[jkey]; 322 | if (j < blo) continue; 323 | if (j >= bhi) break; 324 | newj2len[j] = k = difflib.__dictget(j2len, j - 1, 0) + 1; 325 | if (k > bestsize) { 326 | besti = i - k + 1; 327 | bestj = j - k + 1; 328 | bestsize = k; 329 | } 330 | } 331 | } 332 | j2len = newj2len; 333 | } 334 | 335 | while (besti > alo && bestj > blo && !isbjunk(b[bestj - 1]) && a[besti - 1] == b[bestj - 1]) { 336 | besti--; 337 | bestj--; 338 | bestsize++; 339 | } 340 | 341 | while (besti + bestsize < ahi && bestj + bestsize < bhi && 342 | !isbjunk(b[bestj + bestsize]) && 343 | a[besti + bestsize] == b[bestj + bestsize]) { 344 | bestsize++; 345 | } 346 | 347 | while (besti > alo && bestj > blo && isbjunk(b[bestj - 1]) && a[besti - 1] == b[bestj - 1]) { 348 | besti--; 349 | bestj--; 350 | bestsize++; 351 | } 352 | 353 | while (besti + bestsize < ahi && bestj + bestsize < bhi && isbjunk(b[bestj + bestsize]) && 354 | a[besti + bestsize] == b[bestj + bestsize]) { 355 | bestsize++; 356 | } 357 | 358 | return [besti, bestj, bestsize]; 359 | } 360 | 361 | this.get_matching_blocks = function () { 362 | if (this.matching_blocks != null) return this.matching_blocks; 363 | var la = this.a.length; 364 | var lb = this.b.length; 365 | 366 | var queue = [[0, la, 0, lb]]; 367 | var matching_blocks = []; 368 | var alo, ahi, blo, bhi, qi, i, j, k, x; 369 | while (queue.length) { 370 | qi = queue.pop(); 371 | alo = qi[0]; 372 | ahi = qi[1]; 373 | blo = qi[2]; 374 | bhi = qi[3]; 375 | x = this.find_longest_match(alo, ahi, blo, bhi); 376 | i = x[0]; 377 | j = x[1]; 378 | k = x[2]; 379 | 380 | if (k) { 381 | matching_blocks.push(x); 382 | if (alo < i && blo < j) 383 | queue.push([alo, i, blo, j]); 384 | if (i+k < ahi && j+k < bhi) 385 | queue.push([i + k, ahi, j + k, bhi]); 386 | } 387 | } 388 | 389 | matching_blocks.sort(difflib.__ntuplecomp); 390 | 391 | var i1 = 0, j1 = 0, k1 = 0, block = 0; 392 | var i2, j2, k2; 393 | var non_adjacent = []; 394 | for (var idx in matching_blocks) { 395 | if (matching_blocks.hasOwnProperty(idx)) { 396 | block = matching_blocks[idx]; 397 | i2 = block[0]; 398 | j2 = block[1]; 399 | k2 = block[2]; 400 | if (i1 + k1 == i2 && j1 + k1 == j2) { 401 | k1 += k2; 402 | } else { 403 | if (k1) non_adjacent.push([i1, j1, k1]); 404 | i1 = i2; 405 | j1 = j2; 406 | k1 = k2; 407 | } 408 | } 409 | } 410 | 411 | if (k1) non_adjacent.push([i1, j1, k1]); 412 | 413 | non_adjacent.push([la, lb, 0]); 414 | this.matching_blocks = non_adjacent; 415 | return this.matching_blocks; 416 | } 417 | 418 | this.get_opcodes = function () { 419 | if (this.opcodes != null) return this.opcodes; 420 | var i = 0; 421 | var j = 0; 422 | var answer = []; 423 | this.opcodes = answer; 424 | var block, ai, bj, size, tag; 425 | var blocks = this.get_matching_blocks(); 426 | for (var idx in blocks) { 427 | if (blocks.hasOwnProperty(idx)) { 428 | block = blocks[idx]; 429 | ai = block[0]; 430 | bj = block[1]; 431 | size = block[2]; 432 | tag = ''; 433 | if (i < ai && j < bj) { 434 | tag = 'replace'; 435 | } else if (i < ai) { 436 | tag = 'delete'; 437 | } else if (j < bj) { 438 | tag = 'insert'; 439 | } 440 | if (tag) answer.push([tag, i, ai, j, bj]); 441 | i = ai + size; 442 | j = bj + size; 443 | 444 | if (size) answer.push(['equal', ai, i, bj, j]); 445 | } 446 | } 447 | 448 | return answer; 449 | } 450 | 451 | // this is a generator function in the python lib, which of course is not supported in javascript 452 | // the reimplementation builds up the grouped opcodes into a list in their entirety and returns that. 453 | this.get_grouped_opcodes = function (n) { 454 | if (!n) n = 3; 455 | var codes = this.get_opcodes(); 456 | if (!codes) codes = [["equal", 0, 1, 0, 1]]; 457 | var code, tag, i1, i2, j1, j2; 458 | if (codes[0][0] == 'equal') { 459 | code = codes[0]; 460 | tag = code[0]; 461 | i1 = code[1]; 462 | i2 = code[2]; 463 | j1 = code[3]; 464 | j2 = code[4]; 465 | codes[0] = [tag, Math.max(i1, i2 - n), i2, Math.max(j1, j2 - n), j2]; 466 | } 467 | if (codes[codes.length - 1][0] == 'equal') { 468 | code = codes[codes.length - 1]; 469 | tag = code[0]; 470 | i1 = code[1]; 471 | i2 = code[2]; 472 | j1 = code[3]; 473 | j2 = code[4]; 474 | codes[codes.length - 1] = [tag, i1, Math.min(i2, i1 + n), j1, Math.min(j2, j1 + n)]; 475 | } 476 | 477 | var nn = n + n; 478 | var group = []; 479 | var groups = []; 480 | for (var idx in codes) { 481 | if (codes.hasOwnProperty(idx)) { 482 | code = codes[idx]; 483 | tag = code[0]; 484 | i1 = code[1]; 485 | i2 = code[2]; 486 | j1 = code[3]; 487 | j2 = code[4]; 488 | if (tag == 'equal' && i2 - i1 > nn) { 489 | group.push([tag, i1, Math.min(i2, i1 + n), j1, Math.min(j2, j1 + n)]); 490 | groups.push(group); 491 | group = []; 492 | i1 = Math.max(i1, i2-n); 493 | j1 = Math.max(j1, j2-n); 494 | } 495 | 496 | group.push([tag, i1, i2, j1, j2]); 497 | } 498 | } 499 | 500 | if (group && !(group.length == 1 && group[0][0] == 'equal')) groups.push(group) 501 | 502 | return groups; 503 | } 504 | 505 | this.ratio = function () { 506 | matches = difflib.__reduce( 507 | function (sum, triple) { return sum + triple[triple.length - 1]; }, 508 | this.get_matching_blocks(), 0); 509 | return difflib.__calculate_ratio(matches, this.a.length + this.b.length); 510 | } 511 | 512 | this.quick_ratio = function () { 513 | var fullbcount, elt; 514 | if (this.fullbcount == null) { 515 | this.fullbcount = fullbcount = {}; 516 | for (var i = 0; i < this.b.length; i++) { 517 | elt = this.b[i]; 518 | fullbcount[elt] = difflib.__dictget(fullbcount, elt, 0) + 1; 519 | } 520 | } 521 | fullbcount = this.fullbcount; 522 | 523 | var avail = {}; 524 | var availhas = difflib.__isindict(avail); 525 | var matches = numb = 0; 526 | for (var i = 0; i < this.a.length; i++) { 527 | elt = this.a[i]; 528 | if (availhas(elt)) { 529 | numb = avail[elt]; 530 | } else { 531 | numb = difflib.__dictget(fullbcount, elt, 0); 532 | } 533 | avail[elt] = numb - 1; 534 | if (numb > 0) matches++; 535 | } 536 | 537 | return difflib.__calculate_ratio(matches, this.a.length + this.b.length); 538 | } 539 | 540 | this.real_quick_ratio = function () { 541 | var la = this.a.length; 542 | var lb = this.b.length; 543 | return _calculate_ratio(Math.min(la, lb), la + lb); 544 | } 545 | 546 | this.isjunk = isjunk ? isjunk : difflib.defaultJunkFunction; 547 | this.a = this.b = null; 548 | this.set_seqs(a, b); 549 | } 550 | }; 551 | 552 | 553 | // finally export difflib 554 | Difflib.lib = difflib; 555 | Difflib.view = diffview; 556 | -------------------------------------------------------------------------------- /packages/difflib/package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | version: "1.0.0", 3 | name: "jeremywrnr:difflib", 4 | summary: "Difference generator for file contents.", 5 | git: "https://github.com/jeremywrnr/git-sync", 6 | }); 7 | 8 | 9 | Package.onUse(function(api) { 10 | api.export("Difflib"); 11 | api.versionsFrom("METEOR@1.3"); 12 | api.addFiles(["difflib.js"]); 13 | }); 14 | 15 | 16 | Package.onTest(function (api) { 17 | api.use(["tinytest", "ecmascript", "jeremywrnr:difflib"]); 18 | api.addFiles("difflib-tests.js"); 19 | }); 20 | -------------------------------------------------------------------------------- /packages/firepad/firepad-tests.js: -------------------------------------------------------------------------------- 1 | Tinytest.add("smoke", function (test) { 2 | console.log("starting firepad smoke") 3 | test.equal(true, true) // starting up 4 | //var cb = function () {}; 5 | //FirepadAPI.setup(true); 6 | //FirepadAPI.userfiles(); 7 | //FirepadAPI.setText(0); 8 | //FirepadAPI.setAllText(); 9 | //FirepadAPI.getText(0, cb); 10 | //FirepadAPI.getAllText([0, 1]); 11 | }); 12 | -------------------------------------------------------------------------------- /packages/firepad/firepad.js: -------------------------------------------------------------------------------- 1 | // interface for interacting with Firepad through meteor 2 | // most methods have to be called from client since jsdom cant run on the 3 | // server, which means that firepad cant run on the meteor backend. 4 | 5 | FirepadAPI = {} 6 | 7 | // getting whether the host is in production or development 8 | 9 | var setup = function (dev) { 10 | var prodFB = "https://project-3627267568762325747.firebaseio.com/" 11 | var devFB = "https://gitsync-test.firebaseio.com/" 12 | this.host = (dev ? devFB : prodFB); 13 | } 14 | 15 | // return the current branch/repo files 16 | 17 | var userfiles = function () { 18 | var user = Meteor.user(), 19 | prof = undefined; 20 | if (user) 21 | prof = user.profile; 22 | if (prof) return Files.find({ 23 | repo: user.repo, 24 | branch: user.repoBranch 25 | }); 26 | } 27 | 28 | 29 | /*** 30 | * |GET| 31 | * 32 | * FIREPAD -> file.CONTENT methods 33 | * - update files meteor content based on the firepad buffer 34 | * - also used when updating the tester vision 35 | * - this methods are called on the client to have access to firepad from the 36 | * header scripts, and then use a meteor method callback to gain access to 37 | * updating the files once the content has been retrieved from the firepad 38 | * backend. i am actually largely impressed with how robust and fast 39 | * firepad seems to work since transitioning over from the internal sharejs 40 | * editor. maybe it is the snapshot feature, which limits how many 41 | * transformations have to be performed to the get the current state of the 42 | * document. 43 | ***/ 44 | 45 | var getText = function (id, cb) { // return the contents of firepad 46 | var headless = Firepad.Headless(Session.get("fb") + id); 47 | headless.getText(function(txt) { 48 | headless.dispose(); 49 | cb(txt); 50 | }); 51 | } 52 | 53 | var getAllText = function(files, cb) { // apply callmback to all files 54 | files.map(function(id) { 55 | FirepadAPI.getText(id, function(txt) { 56 | cb(id, txt); 57 | }); 58 | }); 59 | } 60 | 61 | 62 | /*** 63 | * |SET| 64 | * 65 | * file.CACHE -> FIREPAD methods 66 | * - update firepad buffer from last committed version of file 67 | * - gets content based on the cached version from last commit 68 | * - dispose removes the connection to the firepad instance. 69 | ***/ 70 | 71 | var setText = function (id, cb) { // update firebase with their ids 72 | var headless = Firepad.Headless(Session.get("fb") + id); 73 | headless.setText( 74 | Files.findOne(id).cache, 75 | function() { 76 | headless.dispose() 77 | cb(); // callback once done 78 | } 79 | ); 80 | } 81 | 82 | var setAllText = function (cb) { // update all project caches from firepad (for reset) 83 | FirepadAPI.userfiles().fetch().map(function(file) { 84 | // TODO if the last one - then pass in the callback 85 | FirepadAPI.setText(file._id) 86 | }); 87 | 88 | cb(); // callback once done 89 | } 90 | 91 | 92 | // exporting to package 93 | FirepadAPI.setup = setup; 94 | FirepadAPI.getText = getText; 95 | FirepadAPI.setText = setText; 96 | FirepadAPI.userfiles = userfiles; 97 | FirepadAPI.getAllText = getAllText; 98 | FirepadAPI.setAllText = setAllText; 99 | 100 | -------------------------------------------------------------------------------- /packages/firepad/package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | version: "1.0.0", 3 | name: "jeremywrnr:firepad", 4 | summary: "Interface for using firepad easily.", 5 | git: "https://github.com/jeremywrnr/git-sync", 6 | }); 7 | 8 | 9 | Package.onUse(function(api) { 10 | api.export("FirepadAPI"); 11 | api.versionsFrom("METEOR@1.3"); 12 | api.addFiles(["firepad.js"]); 13 | }); 14 | 15 | 16 | Package.onTest(function (api) { 17 | api.use(["tinytest", "ecmascript", "jeremywrnr:firepad"]); 18 | api.addFiles("firepad-tests.js"); 19 | }); 20 | 21 | -------------------------------------------------------------------------------- /packages/git-sync/git-sync-tests.js: -------------------------------------------------------------------------------- 1 | // linkify tests 2 | 3 | Tinytest.add("linkify", function (test) { 4 | var link = function(x) { return '' + x + '' } 5 | test.equal(true, true) 6 | 7 | var x = GitSync.linkify("x") 8 | test.equal(x, "x") 9 | 10 | var site = "http://y.com" 11 | var y = GitSync.linkify(site) 12 | test.equal(y, link(site)) 13 | 14 | var site1 = "this is a big string with a link inside " 15 | var site2 = "http://hi.net" 16 | var z = GitSync.linkify(site1 + site2) 17 | test.equal(z, site1 + link(site2)) 18 | }); 19 | 20 | -------------------------------------------------------------------------------- /packages/git-sync/git-sync.js: -------------------------------------------------------------------------------- 1 | // global js functions, preloaded into meteor from lib 2 | // includes many functions used throughout templates 3 | 4 | GitSync = { 5 | host: "http://www.git-sync.com/", 6 | 7 | maxFileLength: 15000, 8 | 9 | firebaseSetup: function(dev) { 10 | var prodFB = "https://project-3627267568762325747.firebaseio.com/" 11 | var devFB = "https://gitsync-test.firebaseio.com/" 12 | this.firebase = (dev ? devFB : prodFB) 13 | }, 14 | 15 | any: function(ary, fn) { 16 | return ary.reduce(function(o, n){ 17 | return o || fn(n) 18 | }, false); 19 | }, 20 | 21 | all: function(ary, fn) { 22 | return ary.reduce(function(o, n){ 23 | return o && fn(n) 24 | }, true); 25 | }, 26 | 27 | prof: function() { // return the current users profile 28 | var user = Meteor.user(); 29 | if (user) return user.profile; 30 | }, 31 | 32 | userfiles: function() { // return the current b/r files 33 | var user = GitSync.prof(); 34 | if (user) return Files.find({ 35 | repo: user.repo, 36 | branch: user.repoBranch 37 | }); 38 | }, 39 | 40 | ufids: function() { // return an array of ids of users files 41 | return GitSync.userfiles().fetch().map((f) => f._id); 42 | }, 43 | 44 | changes: function() { // content v cache, check if any files changed 45 | return GitSync.any( 46 | GitSync.userfiles().fetch(), 47 | function(file) { return file.content !== file.cache } 48 | ) 49 | }, 50 | 51 | findFileFromExt: function(ext) { 52 | return Files.findOne({ 53 | title: new RegExp(".*\." + ext, 'i'), 54 | branch: GitSync.prof().repoBranch, 55 | }); 56 | }, 57 | 58 | focusForm: function(id) { // takes id of form, waits til exists, and focuses 59 | setInterval(function() { 60 | if ($(id).length) { 61 | $(id).focus(); 62 | clearInterval(this); 63 | } //wait til element exists, focus 64 | }, 10); // check every 10ms 65 | }, 66 | 67 | grabTagContentsToRender: function(full, tag) { // return parsed html from tag 68 | var doc = $(''); 69 | doc.html( full.content ); 70 | if ($(tag, doc).length > 0) 71 | return $(tag, doc)[0].innerHTML; 72 | else 73 | return ""; 74 | }, 75 | 76 | sanitizeStringQuotes: function(str) { // try to avoid breaking srcdoc 77 | return (str 78 | .replace(/'/g, '"') 79 | .replace(/"/g, '\\"') 80 | .replace(/\n/g, '\\n') 81 | ); 82 | }, 83 | 84 | sanitizeDiffs: function(str) { // make lts / gts into actual spacing 85 | return (str 86 | .replace(/</g, '<') 87 | .replace(/>/g, '>') 88 | ); 89 | }, 90 | 91 | labelLineNumbers: function(text) { // label a chunk of text with line numbers 92 | var doc = $('
');
 93 |     var full = '' + text + '';
 94 |     doc.html( full );
 95 |     var num = text.split(/\n/).length;
 96 | 
 97 |     for (var i = 0; i < num; i++) // for all lines in the file
 98 |       $('span', doc)[0].innerHTML += '' + (i + 1) + '';
 99 |     return doc[0].innerHTML;
100 |   },
101 | 
102 |   linkify: function(str) { // take in string, parse and wrap any links inside
103 |     var domain = /^http.*\.(io|com|web|net|org|gov|edu)(\/.*)?/g
104 | 
105 |     return str.split(' ').map(function linker(s) { // open in new tab, too
106 |       if (s.match(domain))
107 |         return '' + s + ''
108 |       else
109 |         return s
110 |     }).join(' ');
111 |   },
112 | 
113 |   imgcheck: function(title) {
114 |     var image = /\.(gif|jpg|jpeg|tiff|png|bmp|svg|pdf|zip|tar|gz2|rar|bz2|dmg|xz)$/i;
115 | 
116 |     return title.match(image);
117 |   },
118 | 
119 |   tutorMap: {
120 |     "ace/mode/javascript": "js",
121 |     "ace/mode/typescript": "ts",
122 |     "ace/mode/ruby": "ruby",
123 |     "ace/mode/java": "java",
124 |     "ace/mode/c_cpp": "cpp",
125 |     "ace/mode/python": "3",
126 |   },
127 | 
128 |   interactMap: {
129 |     "ace/mode/javascript": "javascript",
130 |     "ace/mode/python": "python",
131 |     "ace/mode/ruby": "ruby",
132 |   },
133 | 
134 |   findFileMode: function(doc) {
135 |       var modelist = ace.require("ace/ext/modelist");
136 |       var file = Files.findOne(doc);
137 |       if (file && modelist)
138 |         return modelist.getModeForPath(file.title).mode;
139 |     },
140 | };
141 | 


--------------------------------------------------------------------------------
/packages/git-sync/package.js:
--------------------------------------------------------------------------------
 1 | Package.describe({
 2 |   version: "1.0.2",
 3 |   name: "jeremywrnr:git-sync",
 4 |   summary: "Real-time pair programming toolset.",
 5 |   git: "https://github.com/jeremywrnr/git-sync",
 6 | });
 7 | 
 8 | 
 9 | Package.onUse(function(api) {
10 |   api.export("GitSync");
11 | 
12 |   api.versionsFrom("METEOR@1.3");
13 |   api.addFiles(["git-sync.js"]);
14 | });
15 | 
16 | 
17 | Package.onTest(function (api) {
18 |   api.use(["tinytest", "ecmascript", "jeremywrnr:git-sync"]);
19 |   api.addFiles("git-sync-tests.js");
20 | });
21 | 
22 | 


--------------------------------------------------------------------------------
/private/development.json.cast5:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremywrnr/codepilot/ceebac93e97e4553df53d6cdcc616f6bd0efbe56/private/development.json.cast5


--------------------------------------------------------------------------------
/private/production.json.cast5:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremywrnr/codepilot/ceebac93e97e4553df53d6cdcc616f6bd0efbe56/private/production.json.cast5


--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremywrnr/codepilot/ceebac93e97e4553df53d6cdcc616f6bd0efbe56/public/favicon.ico


--------------------------------------------------------------------------------
/public/images/commit.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremywrnr/codepilot/ceebac93e97e4553df53d6cdcc616f6bd0efbe56/public/images/commit.gif


--------------------------------------------------------------------------------
/public/images/commit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremywrnr/codepilot/ceebac93e97e4553df53d6cdcc616f6bd0efbe56/public/images/commit.png


--------------------------------------------------------------------------------
/public/images/editor.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremywrnr/codepilot/ceebac93e97e4553df53d6cdcc616f6bd0efbe56/public/images/editor.gif


--------------------------------------------------------------------------------
/public/images/editor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremywrnr/codepilot/ceebac93e97e4553df53d6cdcc616f6bd0efbe56/public/images/editor.png


--------------------------------------------------------------------------------
/public/images/github.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremywrnr/codepilot/ceebac93e97e4553df53d6cdcc616f6bd0efbe56/public/images/github.gif


--------------------------------------------------------------------------------
/public/images/gitlogo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremywrnr/codepilot/ceebac93e97e4553df53d6cdcc616f6bd0efbe56/public/images/gitlogo.gif


--------------------------------------------------------------------------------
/public/images/gitlogo2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremywrnr/codepilot/ceebac93e97e4553df53d6cdcc616f6bd0efbe56/public/images/gitlogo2.gif


--------------------------------------------------------------------------------
/public/images/loading.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremywrnr/codepilot/ceebac93e97e4553df53d6cdcc616f6bd0efbe56/public/images/loading.gif


--------------------------------------------------------------------------------
/public/images/topguntocat.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremywrnr/codepilot/ceebac93e97e4553df53d6cdcc616f6bd0efbe56/public/images/topguntocat.gif


--------------------------------------------------------------------------------
/public/images/visualize.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremywrnr/codepilot/ceebac93e97e4553df53d6cdcc616f6bd0efbe56/public/images/visualize.gif


--------------------------------------------------------------------------------
/public/images/visualize.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremywrnr/codepilot/ceebac93e97e4553df53d6cdcc616f6bd0efbe56/public/images/visualize.png


--------------------------------------------------------------------------------
/public/javascript/feedback.js:
--------------------------------------------------------------------------------
  1 | // feedback.js
  2 | // 2013, Kázmér Rapavi, https://github.com/ivoviz/feedback
  3 | // Licensed under the MIT license.
  4 | // Version 2.0
  5 | 
  6 | (function($){
  7 | 
  8 |   $.feedback = function(options) {
  9 | 
 10 |     var settings = $.extend({
 11 |       ajaxURL:        '',
 12 |       postBrowserInfo:    true,
 13 |       postHTML:       true,
 14 |       postURL:        true,
 15 |       proxy:          undefined,
 16 |       letterRendering:    false,
 17 |       initButtonText:     'Send feedback',
 18 |       strokeStyle:      'black',
 19 |       shadowColor:      'black',
 20 |       shadowOffsetX:      1,
 21 |       shadowOffsetY:      1,
 22 |       shadowBlur:       10,
 23 |       lineJoin:       'bevel',
 24 |       lineWidth:        3,
 25 |       html2canvasURL:     'html2canvas.js',
 26 |       feedbackButton:     '.feedback-btn',
 27 |       showDescriptionModal:   true,
 28 |       isDraggable:      true,
 29 |       onScreenshotTaken:    function(){},
 30 |       tpl: {
 31 | 
 32 |         description: ' 

Feedback lets you send us suggestions about our products. We welcome problem reports, feature ideas and general comments.

Start by writing a brief description:

Next we\'ll let you identify areas of the page related to your description.

Please enter a description.
', 33 | 34 | highlighter: '

Click and drag on the page to help us better understand your feedback. You can move this dialog if it\'s in the way.

', 35 | 36 | overview: '

Description

None
Browser Info
Page Info
Page Structure

Screenshot

Please enter a description.
', 37 | 38 | submitSuccess: '

Thank you for your feedback. We value every piece of feedback we receive.

We cannot respond individually to every one, but we will use your comments as we strive to improve your experience.

', 39 | 40 | submitError: '

Sadly an error occured while sending your feedback. Please try again.

' 41 | 42 | }, 43 | onClose: function() {}, 44 | screenshotStroke: true, 45 | highlightElement: true, 46 | initialBox: false 47 | }, options); 48 | var supportedBrowser = !!window.HTMLCanvasElement; 49 | var isFeedbackButtonNative = settings.feedbackButton == '.feedback-btn'; 50 | var _html2canvas = false; 51 | if (supportedBrowser) { 52 | if(isFeedbackButtonNative) { 53 | $('body').append(''); 54 | } 55 | $(document).on('click', settings.feedbackButton, function(){ 56 | if(isFeedbackButtonNative) { 57 | $(this).hide(); 58 | } 59 | if (!_html2canvas) { 60 | $.getScript(settings.html2canvasURL, function() { 61 | _html2canvas = true; 62 | }); 63 | } 64 | var canDraw = false, 65 | img = '', 66 | h = $(document).height(), 67 | w = $(document).width(), 68 | tpl = '
'; 69 | 70 | if (settings.initialBox) { 71 | tpl += settings.tpl.description; 72 | } 73 | 74 | tpl += settings.tpl.highlighter + settings.tpl.overview + '
'; 75 | 76 | $('body').append(tpl); 77 | 78 | moduleStyle = { 79 | 'position': 'absolute', 80 | 'left': '0px', 81 | 'top': '0px' 82 | }; 83 | canvasAttr = { 84 | 'width': w, 85 | 'height': h 86 | }; 87 | 88 | $('#feedback-module').css(moduleStyle); 89 | $('#feedback-canvas').attr(canvasAttr).css('z-index', '30000'); 90 | 91 | if (!settings.initialBox) { 92 | $('#feedback-highlighter-back').remove(); 93 | canDraw = true; 94 | $('#feedback-canvas').css('cursor', 'crosshair'); 95 | $('#feedback-helpers').show(); 96 | $('#feedback-welcome').hide(); 97 | $('#feedback-highlighter').show(); 98 | } 99 | 100 | if(settings.isDraggable) { 101 | $('#feedback-highlighter').on('mousedown', function(e) { 102 | var $d = $(this).addClass('feedback-draggable'), 103 | drag_h = $d.outerHeight(), 104 | drag_w = $d.outerWidth(), 105 | pos_y = $d.offset().top + drag_h - e.pageY, 106 | pos_x = $d.offset().left + drag_w - e.pageX; 107 | $d.css('z-index', 40000).parents().on('mousemove', function(e) { 108 | _top = e.pageY + pos_y - drag_h; 109 | _left = e.pageX + pos_x - drag_w; 110 | _bottom = drag_h - e.pageY; 111 | _right = drag_w - e.pageX; 112 | 113 | if (_left < 0) _left = 0; 114 | if (_top < 0) _top = 0; 115 | if (_right > $(window).width()) 116 | _left = $(window).width() - drag_w; 117 | if (_left > $(window).width() - drag_w) 118 | _left = $(window).width() - drag_w; 119 | if (_bottom > $(document).height()) 120 | _top = $(document).height() - drag_h; 121 | if (_top > $(document).height() - drag_h) 122 | _top = $(document).height() - drag_h; 123 | 124 | $('.feedback-draggable').offset({ 125 | top: _top, 126 | left: _left 127 | }).on("mouseup", function() { 128 | $(this).removeClass('feedback-draggable'); 129 | }); 130 | }); 131 | e.preventDefault(); 132 | }).on('mouseup', function(){ 133 | $(this).removeClass('feedback-draggable'); 134 | $(this).parents().off('mousemove mousedown'); 135 | }); 136 | } 137 | 138 | var ctx = $('#feedback-canvas')[0].getContext('2d'); 139 | 140 | ctx.fillStyle = 'rgba(102,102,102,0.5)'; 141 | ctx.fillRect(0, 0, $('#feedback-canvas').width(), $('#feedback-canvas').height()); 142 | 143 | rect = {}; 144 | drag = false; 145 | highlight = 1, 146 | post = {}; 147 | 148 | if (settings.postBrowserInfo) { 149 | post.browser = {}; 150 | post.browser.appCodeName = navigator.appCodeName; 151 | post.browser.appName = navigator.appName; 152 | post.browser.appVersion = navigator.appVersion; 153 | post.browser.cookieEnabled = navigator.cookieEnabled; 154 | post.browser.onLine = navigator.onLine; 155 | post.browser.platform = navigator.platform; 156 | post.browser.userAgent = navigator.userAgent; 157 | post.browser.plugins = []; 158 | 159 | $.each(navigator.plugins, function(i) { 160 | post.browser.plugins.push(navigator.plugins[i].name); 161 | }); 162 | $('#feedback-browser-info').show(); 163 | } 164 | 165 | if (settings.postURL) { 166 | post.url = document.URL; 167 | $('#feedback-page-info').show(); 168 | } 169 | 170 | if (settings.postHTML) { 171 | post.html = $('html').html(); 172 | $('#feedback-page-structure').show(); 173 | } 174 | 175 | if (!settings.postBrowserInfo && !settings.postURL && !settings.postHTML) 176 | $('#feedback-additional-none').show(); 177 | 178 | $(document).on('mousedown', '#feedback-canvas', function(e) { 179 | if (canDraw) { 180 | 181 | rect.startX = e.pageX - $(this).offset().left; 182 | rect.startY = e.pageY - $(this).offset().top; 183 | rect.w = 0; 184 | rect.h = 0; 185 | drag = true; 186 | } 187 | }); 188 | 189 | $(document).on('mouseup', function(){ 190 | if (canDraw) { 191 | drag = false; 192 | 193 | var dtop = rect.startY, 194 | dleft = rect.startX, 195 | dwidth = rect.w, 196 | dheight = rect.h; 197 | dtype = 'highlight'; 198 | 199 | if (dwidth == 0 || dheight == 0) return; 200 | 201 | if (dwidth < 0) { 202 | dleft += dwidth; 203 | dwidth *= -1; 204 | } 205 | if (dheight < 0) { 206 | dtop += dheight; 207 | dheight *= -1; 208 | } 209 | 210 | if (dtop + dheight > $(document).height()) 211 | dheight = $(document).height() - dtop; 212 | if (dleft + dwidth > $(document).width()) 213 | dwidth = $(document).width() - dleft; 214 | 215 | if (highlight == 0) 216 | dtype = 'blackout'; 217 | 218 | $('#feedback-helpers').append('
'); 219 | 220 | redraw(ctx); 221 | rect.w = 0; 222 | } 223 | 224 | }); 225 | 226 | $(document).on('mousemove', function(e) { 227 | if (canDraw && drag) { 228 | $('#feedback-highlighter').css('cursor', 'default'); 229 | 230 | rect.w = (e.pageX - $('#feedback-canvas').offset().left) - rect.startX; 231 | rect.h = (e.pageY - $('#feedback-canvas').offset().top) - rect.startY; 232 | 233 | ctx.clearRect(0, 0, $('#feedback-canvas').width(), $('#feedback-canvas').height()); 234 | ctx.fillStyle = 'rgba(102,102,102,0.5)'; 235 | ctx.fillRect(0, 0, $('#feedback-canvas').width(), $('#feedback-canvas').height()); 236 | $('.feedback-helper').each(function() { 237 | if ($(this).attr('data-type') == 'highlight') 238 | drawlines(ctx, parseInt($(this).css('left'), 10), parseInt($(this).css('top'), 10), $(this).width(), $(this).height()); 239 | }); 240 | if (highlight==1) { 241 | drawlines(ctx, rect.startX, rect.startY, rect.w, rect.h); 242 | ctx.clearRect(rect.startX, rect.startY, rect.w, rect.h); 243 | } 244 | $('.feedback-helper').each(function() { 245 | if ($(this).attr('data-type') == 'highlight') 246 | ctx.clearRect(parseInt($(this).css('left'), 10), parseInt($(this).css('top'), 10), $(this).width(), $(this).height()); 247 | }); 248 | $('.feedback-helper').each(function() { 249 | if ($(this).attr('data-type') == 'blackout') { 250 | ctx.fillStyle = 'rgba(0,0,0,1)'; 251 | ctx.fillRect(parseInt($(this).css('left'), 10), parseInt($(this).css('top'), 10), $(this).width(), $(this).height()) 252 | } 253 | }); 254 | if (highlight == 0) { 255 | ctx.fillStyle = 'rgba(0,0,0,0.5)'; 256 | ctx.fillRect(rect.startX, rect.startY, rect.w, rect.h); 257 | } 258 | } 259 | }); 260 | 261 | if (settings.highlightElement) { 262 | var highlighted = [], 263 | tmpHighlighted = [], 264 | hidx = 0; 265 | 266 | $(document).on('mousemove click', '#feedback-canvas',function(e) { 267 | if (canDraw) { 268 | redraw(ctx); 269 | tmpHighlighted = []; 270 | 271 | $('#feedback-canvas').css('cursor', 'crosshair'); 272 | 273 | $('* :not(body,script,iframe,div,section,.feedback-btn,#feedback-module *)').each(function(){ 274 | if ($(this).attr('data-highlighted') === 'true') 275 | return; 276 | 277 | if (e.pageX > $(this).offset().left && e.pageX < $(this).offset().left + $(this).width() && e.pageY > $(this).offset().top + parseInt($(this).css('padding-top'), 10) && e.pageY < $(this).offset().top + $(this).height() + parseInt($(this).css('padding-top'), 10)) { 278 | tmpHighlighted.push($(this)); 279 | } 280 | }); 281 | 282 | var $toHighlight = tmpHighlighted[tmpHighlighted.length - 1]; 283 | 284 | if ($toHighlight && !drag) { 285 | $('#feedback-canvas').css('cursor', 'pointer'); 286 | 287 | var _x = $toHighlight.offset().left - 2, 288 | _y = $toHighlight.offset().top - 2, 289 | _w = $toHighlight.width() + parseInt($toHighlight.css('padding-left'), 10) + parseInt($toHighlight.css('padding-right'), 10) + 6, 290 | _h = $toHighlight.height() + parseInt($toHighlight.css('padding-top'), 10) + parseInt($toHighlight.css('padding-bottom'), 10) + 6; 291 | 292 | if (highlight == 1) { 293 | drawlines(ctx, _x, _y, _w, _h); 294 | ctx.clearRect(_x, _y, _w, _h); 295 | dtype = 'highlight'; 296 | } 297 | 298 | $('.feedback-helper').each(function() { 299 | if ($(this).attr('data-type') == 'highlight') 300 | ctx.clearRect(parseInt($(this).css('left'), 10), parseInt($(this).css('top'), 10), $(this).width(), $(this).height()); 301 | }); 302 | 303 | if (highlight == 0) { 304 | dtype = 'blackout'; 305 | ctx.fillStyle = 'rgba(0,0,0,0.5)'; 306 | ctx.fillRect(_x, _y, _w, _h); 307 | } 308 | 309 | $('.feedback-helper').each(function() { 310 | if ($(this).attr('data-type') == 'blackout') { 311 | ctx.fillStyle = 'rgba(0,0,0,1)'; 312 | ctx.fillRect(parseInt($(this).css('left'), 10), parseInt($(this).css('top'), 10), $(this).width(), $(this).height()); 313 | } 314 | }); 315 | 316 | if (e.type == 'click' && e.pageX == rect.startX && e.pageY == rect.startY) { 317 | $('#feedback-helpers').append('
'); 318 | highlighted.push(hidx); 319 | ++hidx; 320 | redraw(ctx); 321 | } 322 | } 323 | } 324 | }); 325 | } 326 | 327 | $(document).on('mouseleave', 'body,#feedback-canvas', function() { 328 | redraw(ctx); 329 | }); 330 | 331 | $(document).on('mouseenter', '.feedback-helper', function() { 332 | redraw(ctx); 333 | }); 334 | 335 | $(document).on('click', '#feedback-welcome-next', function() { 336 | if ($('#feedback-note').val().length > 0) { 337 | canDraw = true; 338 | $('#feedback-canvas').css('cursor', 'crosshair'); 339 | $('#feedback-helpers').show(); 340 | $('#feedback-welcome').hide(); 341 | $('#feedback-highlighter').show(); 342 | } 343 | else { 344 | $('#feedback-welcome-error').show(); 345 | } 346 | }); 347 | 348 | $(document).on('mouseenter mouseleave', '.feedback-helper', function(e) { 349 | if (drag) 350 | return; 351 | 352 | rect.w = 0; 353 | rect.h = 0; 354 | 355 | if (e.type === 'mouseenter') { 356 | $(this).css('z-index', '30001'); 357 | $(this).append('
'); 358 | $(this).append('
'); 359 | $(this).find('#feedback-close').css({ 360 | 'top' : -1 * ($(this).find('#feedback-close').height() / 2) + 'px', 361 | 'left' : $(this).width() - ($(this).find('#feedback-close').width() / 2) + 'px' 362 | }); 363 | 364 | if ($(this).attr('data-type') == 'blackout') { 365 | /* redraw white */ 366 | ctx.clearRect(0, 0, $('#feedback-canvas').width(), $('#feedback-canvas').height()); 367 | ctx.fillStyle = 'rgba(102,102,102,0.5)'; 368 | ctx.fillRect(0, 0, $('#feedback-canvas').width(), $('#feedback-canvas').height()); 369 | $('.feedback-helper').each(function() { 370 | if ($(this).attr('data-type') == 'highlight') 371 | drawlines(ctx, parseInt($(this).css('left'), 10), parseInt($(this).css('top'), 10), $(this).width(), $(this).height()); 372 | }); 373 | $('.feedback-helper').each(function() { 374 | if ($(this).attr('data-type') == 'highlight') 375 | ctx.clearRect(parseInt($(this).css('left'), 10), parseInt($(this).css('top'), 10), $(this).width(), $(this).height()); 376 | }); 377 | 378 | ctx.clearRect(parseInt($(this).css('left'), 10), parseInt($(this).css('top'), 10), $(this).width(), $(this).height()) 379 | ctx.fillStyle = 'rgba(0,0,0,0.75)'; 380 | ctx.fillRect(parseInt($(this).css('left'), 10), parseInt($(this).css('top'), 10), $(this).width(), $(this).height()); 381 | 382 | ignore = $(this).attr('data-time'); 383 | 384 | /* redraw black */ 385 | $('.feedback-helper').each(function() { 386 | if ($(this).attr('data-time') == ignore) 387 | return true; 388 | if ($(this).attr('data-type') == 'blackout') { 389 | ctx.fillStyle = 'rgba(0,0,0,1)'; 390 | ctx.fillRect(parseInt($(this).css('left'), 10), parseInt($(this).css('top'), 10), $(this).width(), $(this).height()) 391 | } 392 | }); 393 | } 394 | } 395 | else { 396 | $(this).css('z-index','30000'); 397 | $(this).children().remove(); 398 | if ($(this).attr('data-type') == 'blackout') { 399 | redraw(ctx); 400 | } 401 | } 402 | }); 403 | 404 | $(document).on('click', '#feedback-close', function() { 405 | if (settings.highlightElement && $(this).parent().attr('data-highlight-id')) 406 | var _hidx = $(this).parent().attr('data-highlight-id'); 407 | 408 | $(this).parent().remove(); 409 | 410 | if (settings.highlightElement && _hidx) 411 | $('[data-highlight-id="' + _hidx + '"]').removeAttr('data-highlighted').removeAttr('data-highlight-id'); 412 | 413 | redraw(ctx); 414 | }); 415 | 416 | $('#feedback-module').on('click', '.feedback-wizard-close,.feedback-close-btn', function() { 417 | close(); 418 | }); 419 | 420 | $(document).on('keyup', function(e) { 421 | if (e.keyCode == 27) 422 | close(); 423 | }); 424 | 425 | $(document).on('selectstart dragstart', document, function(e) { 426 | e.preventDefault(); 427 | }); 428 | 429 | $(document).on('click', '#feedback-highlighter-back', function() { 430 | canDraw = false; 431 | $('#feedback-canvas').css('cursor', 'default'); 432 | $('#feedback-helpers').hide(); 433 | $('#feedback-highlighter').hide(); 434 | $('#feedback-welcome-error').hide(); 435 | $('#feedback-welcome').show(); 436 | }); 437 | 438 | $(document).on('mousedown', '.feedback-sethighlight', function() { 439 | highlight = 1; 440 | $(this).addClass('feedback-active'); 441 | $('.feedback-setblackout').removeClass('feedback-active'); 442 | }); 443 | 444 | $(document).on('mousedown', '.feedback-setblackout', function() { 445 | highlight = 0; 446 | $(this).addClass('feedback-active'); 447 | $('.feedback-sethighlight').removeClass('feedback-active'); 448 | }); 449 | 450 | $(document).on('click', '#feedback-highlighter-next', function() { 451 | canDraw = false; 452 | $('#feedback-canvas').css('cursor', 'default'); 453 | var sy = $(document).scrollTop(), 454 | dh = $(window).height(); 455 | $('#feedback-helpers').hide(); 456 | $('#feedback-highlighter').hide(); 457 | if (!settings.screenshotStroke) { 458 | redraw(ctx, false); 459 | } 460 | html2canvas($('body'), { 461 | onrendered: function(canvas) { 462 | if (!settings.screenshotStroke) { 463 | redraw(ctx); 464 | } 465 | _canvas = $('').hide().appendTo('body'); 466 | _ctx = _canvas.get(0).getContext('2d'); 467 | _ctx.drawImage(canvas, 0, sy, w, dh, 0, 0, w, dh); 468 | img = _canvas.get(0).toDataURL(); 469 | $(document).scrollTop(sy); 470 | post.img = img; 471 | settings.onScreenshotTaken(post.img); 472 | if(settings.showDescriptionModal) { 473 | $('#feedback-canvas-tmp').remove(); 474 | $('#feedback-overview').show(); 475 | $('#feedback-overview-description-text>textarea').remove(); 476 | $('#feedback-overview-screenshot>img').remove(); 477 | $('').insertAfter('#feedback-overview-description-text h3:eq(0)'); 478 | $('#feedback-overview-screenshot').append(''); 479 | } 480 | else { 481 | $('#feedback-module').remove(); 482 | close(); 483 | _canvas.remove(); 484 | } 485 | }, 486 | proxy: settings.proxy, 487 | letterRendering: settings.letterRendering 488 | }); 489 | }); 490 | 491 | $(document).on('click', '#feedback-overview-back', function(e) { 492 | canDraw = true; 493 | $('#feedback-canvas').css('cursor', 'crosshair'); 494 | $('#feedback-overview').hide(); 495 | $('#feedback-helpers').show(); 496 | $('#feedback-highlighter').show(); 497 | $('#feedback-overview-error').hide(); 498 | }); 499 | 500 | $(document).on('keyup', '#feedback-note-tmp,#feedback-overview-note', function(e) { 501 | var tx; 502 | if (e.target.id === 'feedback-note-tmp') 503 | tx = $('#feedback-note-tmp').val(); 504 | else { 505 | tx = $('#feedback-overview-note').val(); 506 | $('#feedback-note-tmp').val(tx); 507 | } 508 | 509 | $('#feedback-note').val(tx); 510 | }); 511 | 512 | $(document).on('click', '#feedback-submit', function() { 513 | canDraw = false; 514 | 515 | if ($('#feedback-note').val().length > 0) { 516 | $('#feedback-submit-success,#feedback-submit-error').remove(); 517 | $('#feedback-overview').hide(); 518 | 519 | post.img = img; 520 | 521 | // attach addition all post arguments from options 522 | post.repo = options.repo; 523 | post.user = options.user; 524 | post.html = options.html; // overwriting!! defined earlier 525 | post.css = options.css; 526 | post.js = options.js; 527 | post.log = $('pre#log').text(); // get log contents 528 | post.note = $('#feedback-note').val(); 529 | var data = {feedback: JSON.stringify(post)}; 530 | 531 | $.ajax({ 532 | url: settings.ajaxURL, 533 | dataType: 'json', 534 | type: 'POST', 535 | data: data, 536 | success: function() { 537 | $('#feedback-module').append(settings.tpl.submitSuccess); 538 | $('div.feedback-wizard-close').click(); 539 | }, 540 | error: function(){ 541 | $('#feedback-module').append(settings.tpl.submitError); 542 | $('div.feedback-wizard-close').click(); 543 | } 544 | }); 545 | } 546 | else { 547 | $('#feedback-overview-error').show(); 548 | } 549 | 550 | 551 | }); 552 | }); 553 | } 554 | 555 | function close() { 556 | canDraw = false; 557 | $(document).off('mouseenter mouseleave', '.feedback-helper'); 558 | $(document).off('mouseup keyup'); 559 | $(document).off('mousedown', '.feedback-setblackout'); 560 | $(document).off('mousedown', '.feedback-sethighlight'); 561 | $(document).off('mousedown click', '#feedback-close'); 562 | $(document).off('mousedown', '#feedback-canvas'); 563 | $(document).off('click', '#feedback-highlighter-next'); 564 | $(document).off('click', '#feedback-highlighter-back'); 565 | $(document).off('click', '#feedback-welcome-next'); 566 | $(document).off('click', '#feedback-overview-back'); 567 | $(document).off('mouseleave', 'body'); 568 | $(document).off('mouseenter', '.feedback-helper'); 569 | $(document).off('selectstart dragstart', document); 570 | $('#feedback-module').off('click', '.feedback-wizard-close,.feedback-close-btn'); 571 | $(document).off('click', '#feedback-submit'); 572 | 573 | if (settings.highlightElement) { 574 | $(document).off('click', '#feedback-canvas'); 575 | $(document).off('mousemove', '#feedback-canvas'); 576 | } 577 | $('[data-highlighted="true"]').removeAttr('data-highlight-id').removeAttr('data-highlighted'); 578 | $('#feedback-module').remove(); 579 | $('.feedback-btn').show(); 580 | 581 | settings.onClose.call(this); 582 | } 583 | 584 | function redraw(ctx, border) { 585 | border = typeof border !== 'undefined' ? border : true; 586 | ctx.clearRect(0, 0, $('#feedback-canvas').width(), $('#feedback-canvas').height()); 587 | ctx.fillStyle = 'rgba(102,102,102,0.5)'; 588 | ctx.fillRect(0, 0, $('#feedback-canvas').width(), $('#feedback-canvas').height()); 589 | $('.feedback-helper').each(function() { 590 | if ($(this).attr('data-type') == 'highlight') 591 | if (border) 592 | drawlines(ctx, parseInt($(this).css('left'), 10), parseInt($(this).css('top'), 10), $(this).width(), $(this).height()); 593 | }); 594 | $('.feedback-helper').each(function() { 595 | if ($(this).attr('data-type') == 'highlight') 596 | ctx.clearRect(parseInt($(this).css('left'), 10), parseInt($(this).css('top'), 10), $(this).width(), $(this).height()); 597 | }); 598 | $('.feedback-helper').each(function() { 599 | if ($(this).attr('data-type') == 'blackout') { 600 | ctx.fillStyle = 'rgba(0,0,0,1)'; 601 | ctx.fillRect(parseInt($(this).css('left'), 10), parseInt($(this).css('top'), 10), $(this).width(), $(this).height()); 602 | } 603 | }); 604 | } 605 | 606 | function drawlines(ctx, x, y, w, h) { 607 | ctx.strokeStyle = settings.strokeStyle; 608 | ctx.shadowColor = settings.shadowColor; 609 | ctx.shadowOffsetX = settings.shadowOffsetX; 610 | ctx.shadowOffsetY = settings.shadowOffsetY; 611 | ctx.shadowBlur = settings.shadowBlur; 612 | ctx.lineJoin = settings.lineJoin; 613 | ctx.lineWidth = settings.lineWidth; 614 | 615 | ctx.strokeRect(x,y,w,h); 616 | 617 | ctx.shadowOffsetX = 0; 618 | ctx.shadowOffsetY = 0; 619 | ctx.shadowBlur = 0; 620 | ctx.lineWidth = 1; 621 | } 622 | 623 | }; 624 | 625 | }(jQuery)); 626 | 627 | -------------------------------------------------------------------------------- /public/javascript/interpreter.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import traceback 3 | 4 | from browser import document as doc 5 | from browser import window, alert, console 6 | 7 | _credits = """ Thanks to CWI, CNRI, BeOpen.com, Zope Corporation and a cast of thousands 8 | for supporting Python development. See www.python.org for more information.""" 9 | 10 | _copyright = """Copyright (c) 2012, Pierre Quentel pierre.quentel@gmail.com 11 | All Rights Reserved. 12 | 13 | Copyright (c) 2001-2013 Python Software Foundation. 14 | All Rights Reserved. 15 | 16 | Copyright (c) 2000 BeOpen.com. 17 | All Rights Reserved. 18 | 19 | Copyright (c) 1995-2001 Corporation for National Research Initiatives. 20 | All Rights Reserved. 21 | 22 | Copyright (c) 1991-1995 Stichting Mathematisch Centrum, Amsterdam. 23 | All Rights Reserved.""" 24 | 25 | _license = """Copyright (c) 2012, Pierre Quentel pierre.quentel@gmail.com 26 | All rights reserved. 27 | 28 | Redistribution and use in source and binary forms, with or without 29 | modification, are permitted provided that the following conditions are met: 30 | 31 | Redistributions of source code must retain the above copyright notice, this 32 | list of conditions and the following disclaimer. Redistributions in binary 33 | form must reproduce the above copyright notice, this list of conditions and 34 | the following disclaimer in the documentation and/or other materials provided 35 | with the distribution. 36 | Neither the name of the nor the names of its contributors may 37 | be used to endorse or promote products derived from this software without 38 | specific prior written permission. 39 | 40 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 41 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 42 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 43 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 44 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 45 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 46 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 47 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 48 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 49 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 50 | POSSIBILITY OF SUCH DAMAGE. 51 | """ 52 | 53 | def credits(): 54 | print(_credits) 55 | credits.__repr__ = lambda:_credits 56 | 57 | def copyright(): 58 | print(_copyright) 59 | copyright.__repr__ = lambda:_copyright 60 | 61 | def license(): 62 | print(_license) 63 | license.__repr__ = lambda:_license 64 | 65 | def write(data): 66 | doc['code'].value += str(data) 67 | 68 | 69 | sys.stdout.write = sys.stderr.write = write 70 | history = [] 71 | current = 0 72 | _status = "main" # or "block" if typing inside a block 73 | 74 | 75 | ### Include some things in the namespace 76 | 77 | before_globals = list(globals().keys()) 78 | 79 | 80 | class Link: 81 | """A linked list. 82 | 83 | >>> s = Link(3, Link(4, Link(5))) 84 | >>> len(s) 85 | 3 86 | >>> s[2] 87 | 5 88 | >>> s 89 | Link(3, Link(4, Link(5))) 90 | >>> s.first = 6 91 | >>> s.second = 7 92 | >>> s.rest.rest = Link.empty 93 | >>> s 94 | Link(6, Link(7)) 95 | """ 96 | empty = () 97 | 98 | def __init__(self, first, rest=empty): 99 | assert rest is Link.empty or isinstance(rest, Link) 100 | self.first = first 101 | self.rest = rest 102 | 103 | def __getitem__(self, i): 104 | if i == 0: 105 | return self.first 106 | else: 107 | return self.rest[i-1] 108 | 109 | def __len__(self): 110 | return 1 + len(self.rest) 111 | 112 | def __repr__(self): 113 | if self.rest: 114 | rest_str = ', ' + repr(self.rest) 115 | else: 116 | rest_str = '' 117 | return 'Link({0}{1})'.format(self.first, rest_str) 118 | 119 | @property 120 | def second(self): 121 | return self.rest.first 122 | 123 | @second.setter 124 | def second(self, value): 125 | self.rest.first = value 126 | 127 | # Trees 128 | class Tree: 129 | """A tree with root as its root value.""" 130 | def __init__(self, root, branches=[]): 131 | self.root = root 132 | for branch in branches: 133 | assert isinstance(branch, Tree) 134 | self.branches = list(branches) 135 | 136 | def __repr__(self): 137 | if self.branches: 138 | branch_str = ', ' + repr(self.branches) 139 | else: 140 | branch_str = '' 141 | return 'Tree({0}{1})'.format(self.root, branch_str) 142 | 143 | def __str__(self): 144 | return '\n'.join(self.indented()) 145 | 146 | def indented(self, k=0): 147 | indented = [] 148 | for b in self.branches: 149 | for line in b.indented(k + 1): 150 | indented.append(' ' + line) 151 | return [str(self.root)] + indented 152 | 153 | def is_leaf(self): 154 | return not self.branches 155 | 156 | def leaves(tree): 157 | """Return the leaf values of a tree. 158 | 159 | >>> leaves(fib_tree(4)) 160 | [0, 1, 1, 0, 1] 161 | """ 162 | if tree.is_leaf(): 163 | return [tree.root] 164 | else: 165 | return sum([leaves(b) for b in tree.branches], []) 166 | 167 | # Binary trees 168 | 169 | class BTree(Tree): 170 | """A tree with exactly two branches, which may be empty.""" 171 | empty = Tree(None) 172 | 173 | def __init__(self, root, left=empty, right=empty): 174 | for b in (left, right): 175 | assert isinstance(b, BTree) or b is BTree.empty 176 | Tree.__init__(self, root, (left, right)) 177 | 178 | @property 179 | def left(self): 180 | return self.branches[0] 181 | 182 | @property 183 | def right(self): 184 | return self.branches[1] 185 | 186 | def is_leaf(self): 187 | return [self.left, self.right] == [BTree.empty] * 2 188 | 189 | def __repr__(self): 190 | if self.is_leaf(): 191 | return 'BTree({0})'.format(self.root) 192 | elif self.right is BTree.empty: 193 | left = repr(self.left) 194 | return 'BTree({0}, {1})'.format(self.root, left) 195 | else: 196 | left, right = repr(self.left), repr(self.right) 197 | if self.left is BTree.empty: 198 | left = 'BTree.empty' 199 | template = 'BTree({0}, {1}, {2})' 200 | return template.format(self.root, left, right) 201 | 202 | def fib_tree(n): 203 | """Fibonacci binary tree. 204 | 205 | >>> fib_tree(3) 206 | BTree(2, BTree(1), BTree(1, BTree(0), BTree(1))) 207 | """ 208 | if n == 0 or n == 1: 209 | return BTree(n) 210 | else: 211 | left = fib_tree(n-2) 212 | right = fib_tree(n-1) 213 | fib_n = left.root + right.root 214 | return BTree(fib_n, left, right) 215 | 216 | def contents(t): 217 | """The values in a binary tree. 218 | 219 | >>> contents(fib_tree(5)) 220 | [1, 2, 0, 1, 1, 5, 0, 1, 1, 3, 1, 2, 0, 1, 1] 221 | """ 222 | if t is BTree.empty: 223 | return [] 224 | else: 225 | return contents(t.left) + [t.root] + contents(t.right) 226 | 227 | # Binary search trees 228 | 229 | def bst(values): 230 | """Create a balanced binary search tree from a sorted list. 231 | 232 | >>> bst([1, 3, 5, 7, 9, 11, 13]) 233 | BTree(7, BTree(3, BTree(1), BTree(5)), BTree(11, BTree(9), BTree(13))) 234 | """ 235 | if not values: 236 | return BTree.empty 237 | mid = len(values) // 2 238 | left, right = bst(values[:mid]), bst(values[mid+1:]) 239 | return BTree(values[mid], left, right) 240 | 241 | def largest(t): 242 | """Return the largest element in a binary search tree. 243 | 244 | >>> largest(bst([1, 3, 5, 7, 9])) 245 | 9 246 | """ 247 | if t.right is BTree.empty: 248 | return t.root 249 | else: 250 | return largest(t.right) 251 | 252 | def second(t): 253 | """Return the second largest element in a binary search tree. 254 | 255 | >>> second(bst([1, 3, 5])) 256 | 3 257 | >>> second(bst([1, 3, 5, 7, 9])) 258 | 7 259 | >>> second(Tree(1)) 260 | """ 261 | if t.is_leaf(): 262 | return None 263 | elif t.right is BTree.empty: 264 | return largest(t.left) 265 | elif t.right.is_leaf(): 266 | return t.root 267 | else: 268 | return second(t.right) 269 | 270 | # Sets as binary search trees 271 | 272 | def contains(s, v): 273 | """Return true if set s contains value v as an element. 274 | 275 | >>> t = BTree(2, BTree(1), BTree(3)) 276 | >>> contains(t, 3) 277 | True 278 | >>> contains(t, 0) 279 | False 280 | >>> contains(bst(range(20, 60, 2)), 34) 281 | True 282 | """ 283 | if s is BTree.empty: 284 | return False 285 | elif s.root == v: 286 | return True 287 | elif s.root < v: 288 | return contains(s.right, v) 289 | elif s.root > v: 290 | return contains(s.left, v) 291 | 292 | def adjoin(s, v): 293 | """Return a set containing all elements of s and element v. 294 | 295 | >>> b = bst(range(1, 10, 2)) 296 | >>> adjoin(b, 5) # already contains 5 297 | BTree(5, BTree(3, BTree(1)), BTree(9, BTree(7))) 298 | >>> adjoin(b, 6) 299 | BTree(5, BTree(3, BTree(1)), BTree(9, BTree(7, BTree(6)))) 300 | >>> contents(adjoin(adjoin(b, 6), 2)) 301 | [1, 2, 3, 5, 6, 7, 9] 302 | """ 303 | if s is BTree.empty: 304 | return BTree(v) 305 | elif s.root == v: 306 | return s 307 | elif s.root < v: 308 | return BTree(s.root, s.left, adjoin(s.right, v)) 309 | elif s.root > v: 310 | return BTree(s.root, adjoin(s.left, v), s.right) 311 | 312 | t = BTree(3, BTree(1), 313 | BTree(7, BTree(5), 314 | BTree(9, BTree.empty, BTree(11)))) 315 | 316 | 317 | """Functions that simulate dice rolls. 318 | 319 | A dice function takes no arguments and returns a number from 1 to n 320 | (inclusive), where n is the number of sides on the dice. 321 | 322 | Types of dice: 323 | 324 | - Dice can be fair, meaning that they produce each possible outcome with equal 325 | probability. Examples: four_sided, six_sided 326 | 327 | - For testing functions that use dice, deterministic test dice always cycle 328 | through a fixed sequence of values that are passed as arguments to the 329 | make_test_dice function. 330 | """ 331 | 332 | from random import randint 333 | 334 | def make_fair_dice(sides): 335 | """Return a die that returns 1 to SIDES with equal chance.""" 336 | assert type(sides) == int and sides >= 1, 'Illegal value for sides' 337 | def dice(): 338 | return randint(1,sides) 339 | return dice 340 | 341 | four_sided = make_fair_dice(4) 342 | six_sided = make_fair_dice(6) 343 | 344 | def make_test_dice(*outcomes): 345 | """Return a die that cycles deterministically through OUTCOMES. 346 | 347 | >>> dice = make_test_dice(1, 2, 3) 348 | >>> dice() 349 | 1 350 | >>> dice() 351 | 2 352 | >>> dice() 353 | 3 354 | >>> dice() 355 | 1 356 | >>> dice() 357 | 2 358 | 359 | This function uses Python syntax/techniques not yet covered in this course. 360 | The best way to understand it is by reading the documentation and examples. 361 | """ 362 | assert len(outcomes) > 0, 'You must supply outcomes to make_test_dice' 363 | for o in outcomes: 364 | assert type(o) == int and o >= 1, 'Outcome is not a positive integer' 365 | index = len(outcomes) - 1 366 | def dice(): 367 | nonlocal index 368 | index = (index + 1) % len(outcomes) 369 | return outcomes[index] 370 | return dice 371 | 372 | after_globals = list(globals().keys()) 373 | 374 | custom_globals = set(after_globals) - set(before_globals) 375 | 376 | # execution namespace 377 | editor_ns = {'credits':credits, 378 | 'copyright':copyright, 379 | 'license':license, 380 | '__name__':'__main__', 381 | 'custom_globals': custom_globals 382 | } 383 | 384 | for name in custom_globals: 385 | editor_ns[name] = eval(name) 386 | 387 | def cursorToEnd(*args): 388 | pos = len(doc['code'].value) 389 | doc['code'].setSelectionRange(pos, pos) 390 | doc['code'].scrollTop = doc['code'].scrollHeight 391 | 392 | def get_col(area): 393 | # returns the column num of cursor 394 | sel = doc['code'].selectionStart 395 | lines = doc['code'].value.split('\n') 396 | for line in lines[:-1]: 397 | sel -= len(line) + 1 398 | return sel 399 | 400 | 401 | def set_text_to(text): 402 | doc['code'].value = text 403 | 404 | def myKeyPress(event): 405 | global _status, current 406 | if event.keyCode == 9: # tab key 407 | event.preventDefault() 408 | doc['code'].value += " " 409 | elif event.keyCode == 13: # return 410 | src = doc['code'].value 411 | if _status == "main": 412 | currentLine = src[src.rfind('>>>') + 4:] 413 | elif _status == "3string": 414 | currentLine = src[src.rfind('>>>') + 4:] 415 | currentLine = currentLine.replace('\n... ', '\n') 416 | else: 417 | currentLine = src[src.rfind('...') + 4:] 418 | if _status == 'main' and not currentLine.strip(): 419 | doc['code'].value += '\n>>> ' 420 | event.preventDefault() 421 | return 422 | doc['code'].value += '\n' 423 | history.append(currentLine) 424 | current = len(history) 425 | if _status == "main" or _status == "3string": 426 | try: 427 | _ = editor_ns['_'] = eval(currentLine, editor_ns) 428 | if _ is not None: 429 | write(repr(_)+'\n') 430 | doc['code'].value += '>>> ' 431 | _status = "main" 432 | except IndentationError: 433 | doc['code'].value += '... ' 434 | _status = "block" 435 | except SyntaxError as msg: 436 | if str(msg) == 'invalid syntax : triple string end not found' or \ 437 | str(msg).startswith('Unbalanced bracket'): 438 | doc['code'].value += '... ' 439 | _status = "3string" 440 | elif str(msg) == 'eval() argument must be an expression': 441 | try: 442 | exec(currentLine, editor_ns) 443 | except: 444 | traceback.print_exc() 445 | doc['code'].value += '>>> ' 446 | _status = "main" 447 | elif str(msg) == 'decorator expects function': 448 | doc['code'].value += '... ' 449 | _status = "block" 450 | else: 451 | traceback.print_exc() 452 | doc['code'].value += '>>> ' 453 | _status = "main" 454 | except: 455 | traceback.print_exc() 456 | doc['code'].value += '>>> ' 457 | _status = "main" 458 | elif currentLine == "": # end of block 459 | block = src[src.rfind('>>>') + 4:].splitlines() 460 | block = [block[0]] + [b[4:] for b in block[1:]] 461 | block_src = '\n'.join(block) 462 | # status must be set before executing code in globals() 463 | _status = "main" 464 | try: 465 | _ = exec(block_src, editor_ns) 466 | if _ is not None: 467 | print(repr(_)) 468 | except: 469 | traceback.print_exc() 470 | doc['code'].value += '>>> ' 471 | else: 472 | doc['code'].value += '... ' 473 | 474 | cursorToEnd() 475 | event.preventDefault() 476 | 477 | def myKeyDown(event): 478 | global _status, current 479 | if event.keyCode == 37: # left arrow 480 | sel = get_col(doc['code']) 481 | if sel < 5: 482 | event.preventDefault() 483 | event.stopPropagation() 484 | elif event.keyCode == 36: # line start 485 | pos = doc['code'].selectionStart 486 | col = get_col(doc['code']) 487 | doc['code'].setSelectionRange(pos - col + 4, pos - col + 4) 488 | event.preventDefault() 489 | elif event.keyCode == 38: # up 490 | if current > 0: 491 | pos = doc['code'].selectionStart 492 | col = get_col(doc['code']) 493 | # remove current line 494 | doc['code'].value = doc['code'].value[:pos - col + 4] 495 | current -= 1 496 | doc['code'].value += history[current] 497 | event.preventDefault() 498 | elif event.keyCode == 40: # down 499 | if current < len(history) - 1: 500 | pos = doc['code'].selectionStart 501 | col = get_col(doc['code']) 502 | # remove current line 503 | doc['code'].value = doc['code'].value[:pos - col + 4] 504 | current += 1 505 | doc['code'].value += history[current] 506 | event.preventDefault() 507 | elif event.keyCode == 8: # backspace 508 | src = doc['code'].value 509 | lstart = src.rfind('\n') 510 | if (lstart == -1 and len(src) < 5) or (len(src) - lstart < 6): 511 | event.preventDefault() 512 | event.stopPropagation() 513 | 514 | 515 | doc['code'].bind('keypress', myKeyPress) 516 | doc['code'].bind('keydown', myKeyDown) 517 | doc['code'].bind('click', cursorToEnd) 518 | doc['code'].bind('click', cursorToEnd) 519 | 520 | v = sys.implementation.version 521 | doc['code'].value = "CS61A Python %s.%s.%s\n>>> " % ( 522 | v[0], v[1], v[2]) 523 | #doc['code'].value += 'Type "copyright", "credits" or "license" for more information.' 524 | doc['code'].focus() 525 | cursorToEnd() 526 | -------------------------------------------------------------------------------- /public/style/feedback.min.css: -------------------------------------------------------------------------------- 1 | .feedback-btn{font-size:14px;position:fixed;bottom:-3px;right:60px;width:auto;} 2 | #feedback-module p{font-size:13px;} 3 | #feedback-note-tmp{width:444px;height:auto;min-height:90px;outline:none;font-family: Arial,sans-serif;padding:4px;} 4 | #feedback-note-tmp:focus,#feedback-overview-note:focus{border:1px solid #64b7cc;} 5 | #feedback-canvas{position:absolute;top:0;left:0;} 6 | #feedback-welcome{top:30%;left:50%;margin-left:-270px;display:block;position:fixed;} 7 | .feedback-logo{background:url(icons.png) -0px -0px no-repeat;width:34px;margin-bottom:16px;font-size:16px;font-weight:normal;line-height:32px;padding-left:40px;height:32px;} 8 | .feedback-next-btn{width:72px;height:29px;line-height:27px;float:right;font-size:13px;padding:0 8px;} 9 | .feedback-back-btn{width:72px;height:29px;line-height:27px;float:right;font-size:13px;padding:0 8px;margin-right:20px;} 10 | .feedback-submit-btn{width:72px;height:29px;line-height:27px;float:right;font-size:13px;padding:0 8px;} 11 | .feedback-close-btn{width:72px;height:29px;line-height:27px;float:right;font-size:13px;padding:0 8px;} 12 | .feedback-helper{background:rgba(0,0,0,0);cursor:default;} 13 | .feedback-helper[data-type="highlight"]>.feedback-helper-inner{background:rgba(0,68,255,0.1);} 14 | #feedback-close{cursor:pointer;position:absolute;background:url(icons.png) -0px -64px;width:30px;height:30px;} 15 | .feedback-wizard-close{cursor:pointer;position:absolute;top:2px;right:2px;background:url(icons.png) -0px -34px;width:30px;height:30px;opacity:0.65;} 16 | .feedback-wizard-close:hover{opacity:1;} 17 | #feedback-welcome-error,#feedback-overview-error{display:none;color:#f13e3e;float:right;margin-right:30px;font-size:13px;line-height:29px;} 18 | #feedback-overview-error{margin-top:20px;} 19 | #feedback-highlighter{display:none;bottom:100px;right:100px;position:fixed;width:540px;height:275px;} 20 | #feedback-overview{display:none;top:10%;left:50%;margin-left:-420px;position:fixed;width:840px!important;height:auto;} 21 | #feedback-submit-error,#feedback-submit-success{top:30%;left:50%;margin-left:-300px;display:block;position:fixed;width:600px;height:auto;} 22 | .feedback-btn{padding:10px;outline:0;background-clip:padding-box;-webkit-box-shadow:0 4px 16px rgba(0,0,0,.2);-moz-box-shadow:0 4px 16px rgba(0,0,0,.2);box-shadow:0 4px 16px rgba(0,0,0,.2);z-index:40000;} 23 | .feedback-btn-gray{text-align:center;cursor:pointer;font-family:'Open sans';border:1px solid #dcdcdc;border:1px solid rgba(0,0,0,0.1);color:#444;border-radius:2px;background-color:#f5f5f5;background-image:-webkit-linear-gradient(top,#f5f5f5,#f1f1f1);background-image:-moz-linear-gradient(top,#f5f5f5,#f1f1f1);background-image:-ms-linear-gradient(top,#f5f5f5,#f1f1f1);background-image:-o-linear-gradient(top,#f5f5f5,#f1f1f1);background-image:linear-gradient(top,#f5f5f5,#f1f1f1);} 24 | .feedback-btn-gray:hover{color:#333;border:1px solid #c6c6c6;background-color:#f8f8f8;background-image:-webkit-linear-gradient(top,#f8f8f8,#f1f1f1);background-image:-moz-linear-gradient(top,#f8f8f8,#f1f1f1);background-image:-ms-linear-gradient(top,#f8f8f8,#f1f1f1);background-image:-o-linear-gradient(top,#f8f8f8,#f1f1f1);background-image:linear-gradient(top,#f8f8f8,#f1f1f1);} 25 | .feedback-btn-blue{text-align:center;cursor:pointer;font-family:'Open sans';border-radius:2px;background-color:#357ae8;background-image:-webkit-linear-gradient(top,#4d90fe,#357ae8);background-image:-moz-linear-gradient(top,#4d90fe,#357ae8);background-image:-ms-linear-gradient(top,#4d90fe,#357ae8);background-image:-o-linear-gradient(top,#4d90fe,#357ae8);background-image:linear-gradient(top,#4d90fe,#357ae8);border:1px solid #2f5bb7;color:#fff;} 26 | #feedback-note-tmp,#feedback-overview-note{resize:none;} 27 | #feedback-welcome,#feedback-highlighter,#feedback-overview,#feedback-submit-success,#feedback-submit-error{font-family:Arial,sans-serif;z-index:40000;background:#fff;border:1px solid rgba(0,0,0,.333);padding:30px 42px;width:540px;border:1px solid rgba(0,0,0,.333);outline:0;-webkit-box-shadow:0 4px 16px rgba(0,0,0,.2);-moz-box-shadow:0 4px 16px rgba(0,0,0,.2);box-shadow:0 4px 16px rgba(0,0,0,.2);background:#fff;background-clip:padding-box;box-sizing: border-box;-moz-box-sizing: border-box;-webkit-box-sizing: border-box;-webkit-transform: translateZ(0);} 28 | .feedback-sethighlight,.feedback-setblackout{-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;background-color:#f5f5f5;background-image:-webkit-linear-gradient(top,#f5f5f5,#f1f1f1);background-image:-moz-linear-gradient(top,#f5f5f5,#f1f1f1);background-image:-ms-linear-gradient(top,#f5f5f5,#f1f1f1);background-image:-o-linear-gradient(top,#f5f5f5,#f1f1f1);background-image:linear-gradient(top,#f5f5f5,#f1f1f1);color:#444;border:1px solid #dcdcdc;border:1px solid rgba(0,0,0,0.1);-webkit-border-radius:2px;-moz-border-radius:2px;border-radius:2px;cursor:default;font-size:11px;font-weight:bold;text-align:center;white-space:nowrap;margin-right:16px;height:30px;line-height:28px;min-width:90px;outline:0;padding:0 8px;display:inline-block;float:left; 29 | } 30 | .feedback-setblackout{margin-top:10px;clear:both;} 31 | .feedback-sethighlight div{background:url(icons.png) 0px -94px;width:16px;height:16px;margin-top:7px;float:left;} 32 | .feedback-setblackout div{background:url(icons.png) -16px -94px;width:16px;height:16px;margin-top:7px;float:left;} 33 | .feedback-sethighlight:hover,.feedback-setblackout:hover{-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;background-color:#f8f8f8;background-image:-webkit-linear-gradient(top,#f8f8f8,#f1f1f1);background-image:-moz-linear-gradient(top,#f8f8f8,#f1f1f1);background-image:-ms-linear-gradient(top,#f8f8f8,#f1f1f1);background-image:-o-linear-gradient(top,#f8f8f8,#f1f1f1);background-image:linear-gradient(top,#f8f8f8,#f1f1f1);border:1px solid #c6c6c6;color:#333;} 34 | .feedback-active{-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1);background-color:#eee;background-image:-webkit-linear-gradient(top,#eee,#e0e0e0);background-image:-moz-linear-gradient(top,#eee,#e0e0e0);background-image:-ms-linear-gradient(top,#eee,#e0e0e0);background-image:-o-linear-gradient(top,#eee,#e0e0e0);background-image:linear-gradient(top,#eee,#e0e0e0);border:1px solid #ccc;color:#333;} 35 | #feedback-highlighter label {float:left;margin:0 0 0 10px;line-height:30px;font-size:13px;font-weight:normal;} 36 | #feedback-highlighter label.lower{margin-top:10px;} 37 | .feedback-buttons{float:right;margin-top:20px;clear:both;} 38 | #feedback-module h3{font-weight:bold;font-size:15px;margin:8px 0;} 39 | .feedback-additional{margin-bottom:20px!important;} 40 | #feedback-overview-description{float:left;} 41 | #feedback-overview-note{width:314px;padding:4px;height:90px;outline:none;font-family: Arial,sans-serif;} 42 | #feedback-overview-screenshot{float:right;} 43 | .feedback-screenshot{max-width:396px;padding:1px;border:1px solid #adadad;} 44 | #feedback-overview-description-text span{font-size:14px;margin:8px 0;color:#666;padding-left:10px;background:url(icons.png) -30px -34px no-repeat;margin-left:26px;} 45 | #feedback-browser-info,#feedback-page-info,#feedback-page-structure,#feedback-additional-none{margin-top:16px;display:none;} -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | CodePilot 2 | ========= 3 | 4 | [![MIT License](https://img.shields.io/npm/l/alt.svg?style=flat)](http://jeremywrnr.com/mit-license) 5 | 6 | 7 | This is a tool meant to help people collaborate on code more seamlessly by 8 | integrating some core programming tasks into a single web IDE. It also 9 | encourages collaborator awareness without generating onerous distractions, and 10 | can serve as a bridge for people learning to use version control. 11 | 12 | 13 | ## features 14 | 15 | - project-wide synchronous editing (updates in real time) 16 | - testing, both with PythonTutor and our website renderer 17 | - robust GitHub interface (push, pull, checkout, fork, branch) 18 | 19 | 20 | ## development 21 | 22 | First: 23 | 24 | git clone https://github.com/jeremywrnr/codepilot.git 25 | 26 | You will need to register an application key with github in order to login with 27 | their OAuth system - more information on how you can do that [here][oauth]. On 28 | a related note, there is the [github developer program][devel], which I think 29 | you (may?) need to join if you want to register an app - this is free. The 30 | application will look for deployment keys in the `app/private` folder, in 31 | production.json and development.json, respectively. This is what the insides of 32 | those files should resemble: 33 | 34 | { 35 | "service": "github", 36 | "clientId": "YOUR-CLIENT-ID", 37 | "secret": "YOUR-SECRET-ID" 38 | } 39 | 40 | Once this is setup, simply start running it locally: 41 | 42 | meteor 43 | 44 | Toasts: https://atmospherejs.com/chrismbeckett/toastr 45 | 46 | 47 | ## deployment 48 | 49 | This application is currently deployed on Heroku, with the following buildpack 50 | set up to decrypt the secret key information in `private/`. The `ROOT_URL` 51 | variable has to be set to where you are hosting it, beforehand. Then, 52 | `horse-buildpack` is used to install meteor and start up the server. 53 | 54 | - https://github.com/jeremywrnr/heroku-buildpack-run 55 | - https://github.com/AdmitHub/meteor-buildpack-horse 56 | 57 | 58 | ## background 59 | 60 | This project started out as work done for my master's thesis, which can be found 61 | [here](https://jeremywrnr.com/ms-thesis/). 62 | 63 | 64 | [devel]:https://developer.github.com/program/ 65 | [oauth]:https://developer.github.com/v3/oauth/ 66 | 67 | -------------------------------------------------------------------------------- /server/accounts.js: -------------------------------------------------------------------------------- 1 | // setting up a new account with github api 2 | 3 | Accounts.onCreateUser((options, user) => { 4 | const accessToken = user.services.github.accessToken; 5 | var result; 6 | let profile; 7 | var result = Meteor.http.get('https://api.github.com/user', { 8 | headers: { 'User-Agent': 'GitSync' }, 9 | params: { access_token: accessToken } 10 | }); 11 | 12 | if (result.error) throw result.error; 13 | profile = _.pick( 14 | result.data, 'login', 'name', 'avatar_url', 'url', 'email', 'html_url'); 15 | user.profile = profile 16 | 17 | // use default address if none publicly available 18 | if(!user.profile.email) 19 | user.profile.email = `${user.profile.login}@users.noreply.github.com`; 20 | 21 | // use login as name if none publicly available 22 | if(!user.profile.name) 23 | user.profile.name = user.profile.login; 24 | 25 | // set default target repo 26 | user.profile.repoBranch = 'master' 27 | user.profile.repoName = ' click to select your repo!' 28 | user.profile.repoOwner = '' 29 | user.profile.role = 'pilot' 30 | user.profile.repo = '' 31 | return user; 32 | }); 33 | -------------------------------------------------------------------------------- /server/commits.js: -------------------------------------------------------------------------------- 1 | // server (privileged); methods, can run sync. 2 | // so: files, shareJS, and top-level functions 3 | // dlog is debugger log, see server/setup.js 4 | 5 | const ufiles = GitSync.userfiles; 6 | const hoster = GitSync.host; 7 | 8 | Meteor.methods({ 9 | 10 | ///////////////////// 11 | // COMMIT MANAGEMENT 12 | ///////////////////// 13 | 14 | initCommits() { // re-populating the commit log 15 | Meteor.call("getAllCommits").map(c => { 16 | Meteor.call("addCommit", c); 17 | }); 18 | }, 19 | 20 | addCommit(c) { // adds a commit, links to repo + branch 21 | Commits.upsert({ 22 | repo: Meteor.user().profile.repo, 23 | branch: Meteor.user().profile.repoBranch, 24 | sha: c.sha 25 | },{ 26 | $set: { commit: c } 27 | }); 28 | }, 29 | 30 | loadHead(bname) { // load head of branch, from sha 31 | let sha = Meteor.call("getBranch", bname).commit.sha; 32 | console.log(`loading ${bname} @ ${sha}`) 33 | if (sha) Meteor.call("loadCommit", sha); 34 | }, 35 | 36 | loadCommit(sha) { // takes commit sha, loads into sjs 37 | let commitResults = Meteor.call("getCommit", sha); 38 | let treeSHA = commitResults.commit.tree.sha; 39 | let treeResults = Meteor.call("getTree", treeSHA); 40 | 41 | // only load files, not folders/trees 42 | treeResults.tree.forEach(function load(blob) { 43 | if ((!GitSync.imgcheck(blob.path)) && blob.type === "blob") 44 | Meteor.call("getBlob", blob, (err, content) => { 45 | if (err) return console.error(err) 46 | blob.content = content; 47 | if (content && content.length < GitSync.maxFileLength) 48 | Meteor.call("createFile", blob); 49 | }); 50 | }); 51 | }, 52 | 53 | 54 | //////////////////////////////////////////////////////// 55 | // top level function, grab files and commit to github 56 | //////////////////////////////////////////////////////// 57 | 58 | newCommit(msg) { // grab cache content, commit to github 59 | 60 | // getting all file ids, names, and content 61 | let user = Meteor.user().profile; 62 | let bname = user.repoBranch; 63 | let blobs = Files.find({ 64 | repo: Meteor.user().profile.repo, 65 | branch: Meteor.user().profile.repoBranch, 66 | }).fetch().filter(function typeCheck(file) { // remove imgs 67 | return (file.type === "file" || file.type === "blob") && file.content != undefined; 68 | }).map(function makeBlob(file) { // set file cache 69 | Files.update(file._id, {$set: {cache: file.content}}); 70 | return { 71 | content: file.content, 72 | path: file.title, 73 | mode: file.mode, 74 | type: "blob", 75 | }; 76 | }); 77 | 78 | //console.log("blobs are", blobs) 79 | 80 | // get old tree and update it with new shas, post and get that sha 81 | let branch = Meteor.call("getBranch", bname); 82 | let oldTree = Meteor.call("getTree", branch.commit.commit.tree.sha); 83 | if (!oldTree) oldTree = {"sha": ""} // resetting for new file 84 | let newTree = {base: oldTree.sha, tree: blobs}; 85 | let treeSHA = Meteor.call("postTree", newTree); 86 | 87 | // specify author of this commit 88 | let commitAuthor = { 89 | name: user.login, 90 | email: user.email, 91 | date: new Date(), 92 | }; 93 | 94 | // make the new commit result object 95 | let commitResult = Meteor.call("postCommit", { 96 | message: msg, // passed in 97 | author: commitAuthor, 98 | parents: [branch.commit.sha], 99 | tree: treeSHA, 100 | }); 101 | 102 | // update the ref, point to new commmit 103 | Meteor.call("postRef", commitResult); 104 | 105 | // get the latest commit from the branch head 106 | let lastCommit = Meteor.call("getBranch", bname).commit; 107 | 108 | // post into commit db with repo tag 109 | Meteor.call("addCommit", lastCommit); 110 | 111 | // update the feed with new commit 112 | Meteor.call("addMessage", `committed - ${msg}`); 113 | }, 114 | 115 | }); 116 | -------------------------------------------------------------------------------- /server/files.js: -------------------------------------------------------------------------------- 1 | // server files methods 2 | // git-sync - jeremywrnr 3 | 4 | const ufiles = GitSync.userfiles; 5 | const hoster = GitSync.host; 6 | 7 | Meteor.methods({ 8 | 9 | ////////////////// 10 | // FILE MANAGEMENT 11 | ////////////////// 12 | 13 | firebase() { // expose production host for connection 14 | return FirepadAPI.host; // server's version 15 | }, 16 | 17 | newFile() { // create a new unnamed file 18 | return Meteor.call("createFile", { 19 | path: "untitled", 20 | }); 21 | }, 22 | 23 | createFile(file) { // create or update a file, make sjs doc 24 | // handle null cache/contents when createing a file 25 | file.branch = Meteor.user().profile.repoBranch; 26 | file.repo = Meteor.user().profile.repo; 27 | file.path = file.path || "untitled"; 28 | 29 | // update or insert file 30 | let fs = Files.upsert({ 31 | repo: Meteor.user().profile.repo, 32 | branch: Meteor.user().profile.repoBranch, 33 | title: file.path, 34 | },{ $set: { 35 | content: file.content || "", 36 | cache: file.content || "", 37 | mode: file.mode || "100644", 38 | type: file.type || "file", 39 | }}); 40 | 41 | if (fs.insertedId) { // if a new file made, create firepad 42 | //Meteor.call("addMessage", ` created new file ${file.path}`); 43 | return fs.insertedId; 44 | } 45 | }, 46 | 47 | updateAllFiles() { 48 | Files.find({ 49 | repo: Meteor.user().profile.repo, 50 | branch: Meteor.user().profile.repoBranch, 51 | }).fetch().filter(file => // remove imgs 52 | file.type === "file" && file.content != undefined 53 | ).map(file => // set file cache 54 | Files.update(file._id, {$set: {cache: file.content}}) 55 | ); 56 | }, 57 | 58 | renameFile(fileid, name) { // rename a file with id and name 59 | let file = Files.findOne(fileid); 60 | Meteor.call("addMessage", ` renamed file ${file.title} to ${name}`); 61 | Files.update( 62 | fileid, 63 | {$set: { 64 | title: name 65 | }}); 66 | }, 67 | 68 | deleteFile(id) { // with id, delete a file from system 69 | let file = Files.findOne(id); 70 | Meteor.call("addMessage", ` deleted file ${file.title}`); 71 | Files.remove(id); 72 | }, 73 | 74 | setFileType(file, type) { // set the type field of a file 75 | Files.update( 76 | file._id, 77 | {$set: { 78 | type 79 | }}); 80 | }, 81 | 82 | resetFile(id) { // reset file back to cached version 83 | let old = Files.findOne(id); // overwrite content 84 | if (old) 85 | Files.update(id, {$set: {content: old.cache}}); 86 | }, 87 | 88 | resetFiles() { // reset db and hard code simple website structure 89 | ufiles().map(function delFile(f){ Meteor.call("deleteFile", f._id)}); 90 | let base = [{"title":"site.html"},{"title":"site.css"},{"title":"site.js"}]; 91 | base.map(f => { Meteor.call("createFile", f) }); 92 | }, 93 | 94 | }); 95 | -------------------------------------------------------------------------------- /server/github.js: -------------------------------------------------------------------------------- 1 | // wrappers for github api methods 2 | // dlog is debugger log, see server/setup.js 3 | 4 | Meteor.methods({ 5 | 6 | ////////////////////// 7 | // GITHUB GET REQUESTS 8 | ////////////////////// 9 | 10 | ghAuth() { // authenticate for secure api calls 11 | github.authenticate({ 12 | type: "token", 13 | token: Meteor.user().services.github.accessToken 14 | }); 15 | }, 16 | 17 | getAllRepos() { // put them in db, serve to user (no return) 18 | Meteor.call("ghAuth"); // auth for getting all pushable repos 19 | const uid = Meteor.userId(); // userID, used below 20 | github.repos.getAll({ 21 | user: Meteor.user().profile.login, 22 | per_page: 100 23 | //per_page: 1 // for testing 24 | }).map(function attachUser(gr){ // attach user to git repo (gr) 25 | 26 | const repo = Repos.findOne({ id: gr.id }); 27 | if (repo) { // repo already exists 28 | 29 | const attached = (repo.users.indexOf( uid ) > -1); 30 | if (! attached) // not attached, push user to collaborators 31 | Repos.update(repo._id, {$push: {users: uid }}); 32 | 33 | } else { // brand new repo, just insert. 34 | Repos.insert({ id: gr.id, users: [ uid ], repo: gr }); 35 | } 36 | 37 | }); 38 | }, 39 | 40 | getAllIssues(gr) { // return all issues for repo 41 | Meteor.call("ghAuth"); // auth for getting issues 42 | return github.issues.repoIssues({ 43 | user: gr.repo.owner.login, 44 | repo: gr.repo.name, 45 | state: "open", // or closed, etc 46 | }); 47 | }, 48 | 49 | getAllCommits() { // give all commits for branch 50 | Meteor.call("ghAuth"); // auth for private repos 51 | return github.repos.getCommits({ 52 | user: Meteor.user().profile.repoOwner, 53 | repo: Meteor.user().profile.repoName, 54 | sha: Meteor.user().profile.repoBranch, 55 | per_page: 100 56 | }); 57 | }, 58 | 59 | getRepo(owner, repo) { // give github repo res (need to validate first so you can get a private repo) 60 | Meteor.call("ghAuth"); 61 | 62 | let gh = github.repos.get({ 63 | user: owner, 64 | repo: repo, 65 | }); 66 | 67 | const uid = Meteor.userId(); // userID, used below 68 | return [gh].map(function attachUser(gr){ // attach user to git repo (gr) 69 | const repo = Repos.findOne({ "repo.full_name": `${owner}/${repo}`}); 70 | if (repo) { // repo already exists 71 | 72 | const attached = (repo.users.indexOf( uid ) > -1); 73 | if (! attached) // not attached, push user to collaborators 74 | Repos.update(repo._id, {$push: {users: uid }}); 75 | 76 | } else { // brand new repo, just insert. 77 | Repos.insert({ id: gr.id, users: [ uid ], repo: gr }); 78 | } 79 | }) 80 | }, 81 | 82 | getCommit(commitSHA) { // give commit res 83 | Meteor.call("ghAuth"); // auth for private repos 84 | return github.repos.getCommit({ 85 | user: Meteor.user().profile.repoOwner, 86 | repo: Meteor.user().profile.repoName, 87 | sha: commitSHA 88 | }); 89 | }, 90 | 91 | getBranches(gr) { // update all branches for repo 92 | Meteor.call("ghAuth"); // auth for private repos 93 | return github.repos.getBranches({ 94 | user: gr.repo.owner.login, 95 | repo: gr.repo.name 96 | }); 97 | }, 98 | 99 | getBranch(branchName) { // give branch res 100 | Meteor.call("ghAuth"); // auth for private repos 101 | return github.repos.getBranch({ 102 | user: Meteor.user().profile.repoOwner, 103 | repo: Meteor.user().profile.repoName, 104 | branch: branchName 105 | }); 106 | }, 107 | 108 | getTree(treeSHA) { // gives tree res 109 | Meteor.call("ghAuth"); // auth for private repos 110 | return github.gitdata.getTree({ 111 | user: Meteor.user().profile.repoOwner, 112 | repo: Meteor.user().profile.repoName, 113 | sha: treeSHA, 114 | recursive: true // handle folders 115 | }); 116 | }, 117 | 118 | getBlob(blob) { // give a blobs file contents 119 | Meteor.call("ghAuth"); // auth for private repos 120 | return github.gitdata.getBlob({ 121 | headers: {"Accept":"application/vnd.github.VERSION.raw"}, 122 | user: Meteor.user().profile.repoOwner, 123 | repo: Meteor.user().profile.repoName, 124 | sha: blob.sha 125 | }); 126 | }, 127 | 128 | 129 | 130 | /////////////////////// 131 | // GITHUB POST REQUESTS 132 | /////////////////////// 133 | 134 | postIssue(issue) { // takes feedback issue, creates GH issue 135 | // custom login - iframe not given Meteor.user() scope 136 | const user = Meteor.users.findOne(issue.user); 137 | const token = user.services.github.accessToken; 138 | github.authenticate({ type: "token", token }); 139 | return github.issues.create({ // return githubs issue response 140 | user: user.profile.repoOwner, 141 | repo: user.profile.repoName, 142 | title: issue.note, 143 | body: issue.body, 144 | labels: ["bug", "GitSync"] 145 | }); 146 | }, 147 | 148 | postTree(t) { // takes tree, gives tree SHA hash id 149 | Meteor.call("ghAuth"); 150 | return github.gitdata.createTree({ 151 | user: Meteor.user().profile.repoOwner, 152 | repo: Meteor.user().profile.repoName, 153 | base_tree: t.base || "", 154 | tree: t.tree, 155 | }).sha; // beware!! - returns sha, not the entire post response 156 | }, 157 | 158 | postBranch(branch, parent) { // make new branch off current 159 | Meteor.call("ghAuth"); 160 | return github.gitdata.createReference({ 161 | user: Meteor.user().profile.repoOwner, 162 | repo: Meteor.user().profile.repoName, 163 | ref: `refs/heads/${branch}`, // new branch name 164 | sha: parent, // sha hash of parent 165 | }); 166 | }, 167 | 168 | postCommit(c) { // takes commit c, returns gh commit respns. 169 | Meteor.call("ghAuth"); 170 | return github.gitdata.createCommit({ 171 | user: Meteor.user().profile.repoOwner, 172 | repo: Meteor.user().profile.repoName, 173 | message: c.message, 174 | author: c.author, 175 | parents: c.parents, 176 | tree: c.tree, 177 | }); 178 | }, 179 | 180 | postRef(cr) { // takes commit results (cr), updates ref 181 | Meteor.call("ghAuth"); 182 | return github.gitdata.updateReference({ 183 | user: Meteor.user().profile.repoOwner, 184 | repo: Meteor.user().profile.repoName, 185 | ref: `heads/${Meteor.user().profile.repoBranch}`, 186 | sha: cr.sha 187 | }); 188 | }, 189 | 190 | postRepo(owner, repo) { // done to fork a repo for a new user 191 | Meteor.call("ghAuth"); 192 | return github.repos.fork({ 193 | user: owner, 194 | repo: repo 195 | }); 196 | }, 197 | 198 | }); 199 | -------------------------------------------------------------------------------- /server/issues.js: -------------------------------------------------------------------------------- 1 | const ufiles = GitSync.userfiles; 2 | const hoster = GitSync.host; 3 | 4 | Meteor.methods({ 5 | 6 | /////////////////// 7 | // ISSUE MANAGEMENT 8 | /////////////////// 9 | 10 | initIssues() { // re-populating git repo issues 11 | let repo = Repos.findOne(Meteor.user().profile.repo); 12 | if (repo) { 13 | Meteor.call("getAllIssues", repo).map(function load(issue) { 14 | Issues.upsert({ 15 | repo: repo._id, 16 | ghid: issue.id // (from github) 17 | },{ 18 | $set: {issue}, 19 | }); 20 | }); 21 | } 22 | }, 23 | 24 | addIssue(feedback) { // adds a feedback issue to github 25 | feedback.imglink = Async.runSync(done => { // save screens, give id 26 | Screens.insert({img: feedback.img}, (err, id) => { 27 | done(err, id); 28 | }); 29 | }).result; // attach screenshot to this issue 30 | delete feedback.img; // delete redundant png 31 | 32 | // insert a dummy issue to get id, use later in GH issue body txt 33 | let issueId = Async.runSync(done => { 34 | Issues.insert({issue: null}, (err, id) => { 35 | done(err, id); 36 | }); 37 | }).result; // get the id of the newly inserted issue 38 | 39 | // letruct and append the text of the github issue, including links to screenshot and demo 40 | let imglink = `[issue screenshot](${hoster}screenshot/${feedback.imglink})\n`; 41 | let livelink = `[live code here](${hoster}render/${issueId})\n`; 42 | let htmllink = `html:\n\`\`\`html\n${feedback.html}\n\`\`\`\n`; 43 | let csslink = `css:\n\`\`\`css\n${feedback.css}\n\`\`\`\n`; 44 | let jslink = `js:\n\`\`\`js\n${feedback.js}\n\`\`\`\n`; 45 | let loglink = `console log:\n\`\`\`\n${feedback.log}\`\`\`\n`; 46 | feedback.body = imglink + livelink + htmllink + csslink + jslink + loglink; 47 | 48 | // post the issue to github, and get the GH generated content 49 | let issue = Meteor.call("postIssue", feedback); 50 | let ghIssue = { // the entire issue object 51 | _id: issueId, 52 | ghid: issue.id, // (from github) 53 | repo: feedback.repo, // attach repo forming data 54 | feedback, // attach feedback issue data 55 | issue // returned from github call 56 | }; 57 | 58 | // insert complete issue, and add it to the feed 59 | Issues.update(issueId, ghIssue); 60 | Meteor.call( 61 | "addUserMessage", 62 | feedback.user, 63 | `opened issue - ${feedback.note}` 64 | ); 65 | }, 66 | 67 | closeIssue(issue) { // close an issue on github by number 68 | Meteor.call("ghAuth"); 69 | Meteor.call("addMessage", `closed issue - ${issue.issue.title}`); 70 | github.issues.edit({ 71 | user: Meteor.user().profile.repoOwner, 72 | repo: Meteor.user().profile.repoName, 73 | number: issue.issue.number, 74 | state: "closed" 75 | }); 76 | 77 | Issues.remove(issue._id); // remove from the local database 78 | }, 79 | 80 | }); 81 | -------------------------------------------------------------------------------- /server/setup.js: -------------------------------------------------------------------------------- 1 | // global helper functions 2 | prof = () => { // return the current users profile 3 | const user = Meteor.user(); 4 | if (user) return user.profile; 5 | } 6 | 7 | files = () => { // return the current b/r files 8 | const user = Meteor.user(); 9 | if (user) return Files.find({ 10 | repo: user.repo, 11 | branch: user.repoBranch 12 | }); 13 | } 14 | 15 | 16 | // debugging tools 17 | debug = true; 18 | dlog = msg => { if (debug) console.log(msg) } 19 | asrt = (a, b) => { if (debug && a !== b) 20 | throw(`Error: ${a} != ${b}`) } 21 | 22 | 23 | // data publishing 24 | Meteor.publish("repos", userId => Repos.find({users: userId})); 25 | 26 | Meteor.publish("commits", (repoId, branch) => Commits.find({repo: repoId, branch})); 27 | 28 | Meteor.publish("files", (repoId, branch) => Files.find({repo: repoId, branch})); 29 | 30 | Meteor.publish("messages", repoId => Messages.find({repo: repoId}, 31 | {sort: {time: -1}, limit: 50})); 32 | 33 | Meteor.publish("issues", repoId => Issues.find({repo: repoId})); 34 | 35 | Meteor.publish("screens", () => Screens.find({})); 36 | 37 | 38 | // github auth & config 39 | 40 | const inDevelopment = process.env.NODE_ENV === "development"; 41 | 42 | FirepadAPI.setup(inDevelopment); // setup firebase link 43 | 44 | Meteor.startup(() => { // get correct github auth key 45 | ServiceConfiguration.configurations.remove({service: "github"}); 46 | const prodAuth = JSON.parse(Assets.getText("production.json")); 47 | const devAuth = JSON.parse(Assets.getText("development.json")); 48 | const GHAuth = (inDevelopment ? devAuth : prodAuth); 49 | ServiceConfiguration.configurations.insert(GHAuth); 50 | 51 | // node-github setup 52 | github = new GitHub({ 53 | timeout: 5000, 54 | version: "3.0.0", 55 | protocol: "https", 56 | debug, // boolean declared above 57 | headers: { "User-Agent": "GitSync" } 58 | }); 59 | 60 | // oauth for api 5000/hour 61 | github.authenticate({ 62 | type: "oauth", 63 | key: GHAuth.clientId, 64 | secret: GHAuth.secret 65 | }); 66 | }); 67 | --------------------------------------------------------------------------------