├── .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 |
4 | {{> account }}
5 | {{> extras }}
6 |
7 |
8 |
9 |
10 |
13 |
14 |
60 |
61 |
62 |
70 |
71 |
72 |
73 |
74 | {{currentUser.profile.name}}
75 | - {{ currentUser.profile.login }}
76 |
77 | updated {{ lastUpdated }}
78 |
79 |
80 |
81 |
82 |
83 |
84 | {{#if forking}}
85 |
86 |
87 |
88 |
89 |
92 |
93 | cancel
94 |
95 |
96 |
97 | {{else}}
98 |
99 |
100 | fork a repo from github
101 |
102 |
103 | {{/if}}
104 |
105 |
106 |
107 | {{#if typing}}
108 |
109 |
110 |
111 |
112 |
115 |
116 | cancel
117 |
118 |
119 |
120 | {{else}}
121 |
122 |
123 | don't see your repo? click here
124 |
125 |
126 | {{/if}}
127 |
128 |
129 |
130 | {{#if branching}}
131 |
132 |
133 |
134 |
135 |
138 |
139 | cancel
140 |
141 |
142 |
143 | {{else}}
144 |
145 |
146 | make new branch from {{ currentBranch }}
147 |
148 |
149 | {{/if}}
150 |
151 |
152 |
153 |
154 |
155 |
156 | {{ repo.full_name }}
157 |
158 |
159 |
160 |
161 | {{ name }}
162 |
163 |
164 |
165 |
166 |
171 |
172 |
173 |
213 |
214 |
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 |
4 | {{#if nulldoc}}
5 | {{> loadprompt}}
6 | {{else}}
7 | {{> editor }}
8 | {{/if}}
9 |
10 |
11 |
12 | {{#if isImage }}
13 | {{> renderImage }}
14 | {{else}}
15 | {{> filename }}
16 |
17 | {{render}}
18 | {{/if}}
19 |
20 |
21 |
22 |
23 |
24 |
26 |
27 |
28 |
29 |
30 |
31 | click a file to start editing code!
32 |
33 |
34 |
35 | {{> userfiles}}
36 |
37 |
38 |
39 |
40 |
41 | {{#if rename}}
42 |
44 |
46 | {{else}}
47 |
48 |
~/{{title}}
49 |
60 | {{/if}}
61 |
62 |
63 |
64 |
65 | Rendering image: {{ this }}
66 |
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 |
4 |
12 |
13 | {{> messages }}
14 | {{> chatter }}
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | {{#if messageCount}}
25 | {{#each messages}}
26 | {{> message}}
27 | {{/each}}
28 | {{else}}
29 | quiet so far.
30 | {{/if}}
31 |
32 |
33 |
34 |
35 |
36 |
37 | {{name}} {{{linked}}}
38 |
39 |
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 |
4 |
7 |
8 |
22 |
23 |
24 |
31 |
32 | {{> userfiles}}
33 |
34 |
35 |
36 |
37 |
38 | {{#if files.count}}
39 | {{#each files}}
40 | {{> fileitem}}
41 | {{/each}}
42 | {{else}}
43 | no files yet
44 | {{/if}}
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | {{#if current}}
53 |
54 | {{/if}}
55 | {{title}}
56 |
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 |
27 | {{> topnav }}
28 |
29 | {{#if currentUser }}
30 | {{#if userHasRepo }}
31 | {{#if loadingRepo }}
32 |
33 |
34 |
35 |
loading...
36 |
37 |
38 | {{else}}
39 |
40 | {{> sidebar}}
{{> yield}}
41 |
42 |
43 | {{/if}}
44 | {{else}}
45 |
46 |
47 |
import a GitHub repo:
48 | {{> account}}
49 |
50 |
51 | {{/if}}
52 |
53 | {{else}}
54 | {{> login}}
55 | {{/if}}
56 |
57 |
58 |
59 |
63 |
64 |
65 |
66 |
67 |
68 | {{> yield }}
69 |
70 |
71 |
72 |
73 |
74 | {{> navigation }}
75 | {{> loginGithub }}
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 | {{#if userHasRepo }}
86 | {{#each navItems}}
87 |
88 | {{ name }}
89 | {{/each}}
90 |
91 | {{else}}
92 |
git-sync
93 |
94 | {{/if}}
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 | {{#if loggingIn }}
104 |
logging in...
105 | {{else}}{{#if currentUser }}
106 | {{> userLoggedin }}
107 | {{else}}
108 | {{> userLoggedout }}
109 | {{/if}}{{/if}}
110 |
111 |
112 |
113 |
114 | {{ currentUser.profile.login }}
115 |
116 | logout
117 |
118 |
119 |
120 | login with github
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
welcome to git-sync
130 |
this tool helps people collaborate on code seamlessly by importing
131 | GitHub repos, editing them simulatenously, and pushing them back to GitHub!
132 |
133 |
134 |
135 | {{#if loggingIn }}
136 |
logging in...
137 | {{else}}{{#if currentUser }}
138 | {{> userLoggedin }}
139 | {{else}}
140 | {{> userLoggedout }}
141 | {{/if}}{{/if}}
142 |
143 |
144 |
145 |
146 | {{> about }}
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 | VIDEO
157 |
158 |
159 |
160 |
editing
161 |
162 | Project-wide synchronous editing (no more merge conflicts).
163 |
164 | Like Google Docs, but for your entire GitHub repository.
165 |
166 |
167 |
168 |
testing
169 |
170 | In-browser testing, both with a website renderer and
171 |
Python Tutor .
172 |
173 |
174 |
175 |
integration
176 |
177 | Robust
GitHub
178 | interface (diff, commit, push, pull, checkout, fork, and branch).
179 |
180 |
181 |
182 |
about
183 |
184 |
git-sync
is a free, open source tool designed to:
185 |
186 | integrate core programming tasks into a single web IDE
187 | improve parallelization and productivity w/ activity awareness
188 | serve as a bridge for people learning to use version control
189 |
190 |
191 |
192 | made by jeremywrnr
193 | - source on github
195 |
196 |
197 |
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 |
4 |
96 |
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 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | {{> renderPanel }}
12 | {{> raw }}
13 |
14 |
15 |
16 |
17 |
20 |
21 |
24 |
25 |
28 |
29 |
30 | reload
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/client/save/save.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{#if isPilot}}
5 | {{> commitPanel }}
6 | {{/if}}
7 |
8 | {{> history }}
9 |
10 |
11 |
12 | {{#if changes}}
13 |
14 |
15 | {{#each diffs}}
16 | {{> diff}}
17 | {{/each}}
18 |
19 |
20 | {{else}}
21 |
22 |
23 | No changes to be committed yet. Your edits will be shown here once you
24 | change your files and press the refresh button above!
25 |
26 |
27 | {{/if}}
28 |
29 |
30 |
31 |
32 | {{#if committing}}
33 |
34 |
Code saved! Commit in progress...
35 |
36 | {{else}}
37 |
38 |
save with github
39 |
50 |
51 | {{/if}}
52 |
53 |
54 | {{#if committing}}
55 |
56 |
57 |
60 | cancel
61 |
62 |
63 | {{/if}}
64 |
65 | {{> statusPanel }}
66 |
67 |
68 |
69 |
70 |
71 |
79 |
80 |
81 | {{#each commits}}
82 | {{> commit}}
83 | {{/each}}
84 |
85 |
86 |
87 |
88 | {{#if current}}
89 |
90 |
91 | {{ commit.commit.message }}
92 |
93 | {{ commit.author.login }}
94 |
95 |
96 |
97 |
98 |
101 |
102 |
106 |
107 |
108 | import
109 |
110 |
111 |
112 |
113 | {{else}}
114 |
115 |
116 | {{ commit.commit.message }}
117 |
118 | {{ commit.author.login }}
119 |
120 |
121 |
122 | {{/if}}
123 |
124 |
125 |
126 |
127 |
128 | {{title}}
129 |
130 |
131 |
132 |
133 |
134 |
135 | {{#each lines}}
136 | {{> diffline }}
137 | {{/each}}
138 |
139 |
140 |
141 |
142 | {{#if skipped}}
143 |
144 |
145 |
146 | {{else}}{{#if equal}}
147 | {{ newnum }}> {{ content }}
148 |
149 | {{else}}{{#if deleted}}
150 | {{ newnum }}> {{ content }}
151 |
152 | {{else}}{{#if inserted}}
153 | {{ newnum }}> {{ content }}
154 |
155 | {{/if}}{{/if}}{{/if}}{{/if}}
156 |
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 |
4 | {{> testviz}}
5 | {{> testint}}
6 | {{> testweb}}
7 | {{> issues}}
8 |
9 |
10 |
11 |
29 |
30 | {{#if enabled }}
31 | {{#if target }}
32 | {{> testfile }}
33 | {{else}}
34 | {{#if file }}
35 | {{#if lang }}
36 |
38 |
39 | {{else}}
40 |
45 |
46 | {{/if}}
47 | {{else}}
48 |
51 |
52 | {{/if}}
53 | {{/if}}
54 | {{/if}}
55 |
56 |
57 |
58 |
59 |
77 |
78 | {{#if enabled }}
79 | {{#if target }}
80 | {{> testfile }}
81 | {{else}}
82 | {{#if file }}
83 | {{#if python }}
84 | {{> interactPy}}
85 | {{/if}}
86 |
87 | {{#if ruby }}
88 | {{> interactRuby}}
89 | {{/if}}
90 |
91 | {{#if js }}
92 | {{> interactJs}}
93 | {{/if}}
94 |
95 | {{#if unsupported }}
96 |
97 | File (
{{ title }} ) with type (
{{ mode }} ) currently has no
98 | interactive testing support.
Please
change your target file to JS, ruby, or python to use interactive testing.
100 |
101 | {{/if}}
102 |
103 | {{else}}
104 |
107 | {{/if}}
108 | {{/if}}
109 | {{/if}}
110 |
111 |
112 |
113 |
114 |
128 |
129 |
130 |
131 |
150 |
151 | {{#if enabled }}
152 |
153 | {{/if}}
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
169 |
170 |
171 | {{#if issueCount}}
172 | {{#each issues }}
173 | {{> issue }}
174 | {{/each}}
175 | {{else}}
176 | no issues yet.
177 | {{/if}}
178 |
179 |
180 |
181 |
182 |
183 | {{ issue.title }}
184 | {{#each labels }}
185 | {{> issueLabel }}
186 | {{/each}}
187 |
188 |
189 | {{#if current}}
190 |
191 |
197 |
198 |
199 |
200 |
201 | {{/if}}
202 |
203 |
204 |
205 | {{ name }}
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 | python! coming soon!
214 |
215 |
216 |
217 | ruby! coming soon!
218 |
219 |
220 |
221 |
302 |
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
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.
Next Please enter a description.
',
33 |
34 | highlighter: 'Feedback
Click and drag on the page to help us better understand your feedback. You can move this dialog if it\'s in the way.
Highlight Highlight areas relevant to your feedback.
Black out Black out any personal information. Next Back
',
35 |
36 | overview: 'Feedback
Description Additional info None
Browser Info
Page Info
Page Structure
Screenshot Submit Back
Please enter a description.
',
37 |
38 | submitSuccess: 'Feedback
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.
OK
',
39 |
40 | submitError: 'Feedback
Sadly an error occured while sending your feedback. Please try again.
OK
'
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('' + settings.initButtonText + ' ');
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 | [](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 |
--------------------------------------------------------------------------------