├── .appcast.xml ├── .babelrc ├── .editorconfig ├── .eslintrc.yml ├── .github └── CONTRIBUTING.md ├── .gitignore ├── .gitsketchrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── Resources ├── branches.css ├── branches.html ├── branches.js ├── preferences.css ├── preferences.html └── preferences.js ├── assets ├── delete.svg ├── icon.png └── icons │ ├── branches.png │ ├── commit.png │ ├── pull.png │ └── push.png ├── docs ├── FAQ.md ├── README.md ├── getting-started.md ├── git-commands.md ├── git-lfs.md ├── keyboard-shortcut.md └── sketchignore.md ├── example ├── .exportedArtboards │ └── example │ │ ├── Artboard 1@0.5x.png │ │ └── Rectangle@0.5x.png ├── .sketchignore ├── ScreenCast.gif ├── ScreenShotBad.png ├── ScreenShotNice.png ├── example-boards.md └── example.sketch ├── logo.png ├── package-lock.json ├── package.json ├── src ├── commands │ ├── Add.js │ ├── Branches.js │ ├── Commit.js │ ├── Export.js │ ├── Init.js │ ├── OpenTerminal.js │ ├── Preferences.js │ ├── Pull.js │ ├── Push.js │ └── autoExportOnSave.js ├── common.js ├── exportArtboards.js ├── manifest.json └── preferences.js └── webpack.skpm.config.js /.appcast.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | [ 4 | "transform-react-jsx", 5 | { 6 | "pragma": "h" 7 | } 8 | ] 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | extends: 2 | - standard 3 | - standard-preact 4 | - plugin:import/warnings 5 | - plugin:import/errors 6 | - sketch 7 | - prettier 8 | plugins: 9 | - prettier 10 | parserOptions: 11 | ecmaVersion: 2017 12 | sourceType: module 13 | ecmaFeatures: 14 | jsx: true 15 | rules: 16 | import/no-unresolved: [2, { ignore: ["^sketch$"] }] 17 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Request for contributions 2 | 3 | Please contribute to this repository if any of the following is true: 4 | - You have expertise in community development, communication, or education 5 | - You want open source communities to be more collaborative and inclusive 6 | - You want to help lower the burden to first time contributors 7 | 8 | # How to contribute 9 | 10 | Prerequisites: 11 | 12 | - familiarity with [GitHub PRs](https://help.github.com/articles/using-pull-requests) (pull requests) and issues 13 | - knowledge of Markdown for editing `.md` documents 14 | 15 | In particular, this community seeks the following types of contributions: 16 | 17 | - ideas: participate in an Issues thread or start your own to have your voice 18 | heard 19 | - resources: submit a PR to add to [docs README.md](/docs/README.md) with links to related content 20 | - outline sections: help us ensure that this repository is comprehensive. If 21 | there is a topic that is overlooked, please add it, even if it is just a stub 22 | in the form of a header and single sentence. Initially, most things fall into 23 | this category 24 | - write: contribute your expertise in an area by helping us expand the included 25 | content 26 | - copy editing: fix typos, clarify language, and generally improve the quality 27 | of the content 28 | - formatting: help keep content easy to read with consistent formatting 29 | - code: Fix issues or contribute new features to this or any related projects 30 | 31 | # Conduct 32 | 33 | We are committed to providing a friendly, safe and welcoming environment for 34 | all, regardless of gender, sexual orientation, disability, ethnicity, religion, 35 | or similar personal characteristic. 36 | 37 | Please be kind and courteous. There's no need to be mean or rude. 38 | Respect that people have differences of opinion and that every design or 39 | implementation choice carries a trade-off and numerous costs. There is seldom 40 | a right answer, merely an optimal answer given a set of values and 41 | circumstances. 42 | 43 | Please keep unstructured critique to a minimum. If you have solid ideas you 44 | want to experiment with, make a fork and see how it works. 45 | 46 | We will exclude you from interaction if you insult, demean or harass anyone. 47 | That is not welcome behavior. We interpret the term "harassment" as 48 | including the definition in the 49 | [Citizen Code of Conduct](http://citizencodeofconduct.org/); 50 | if you have any lack of clarity about what might be included in that concept, 51 | please read their definition. In particular, we don't tolerate behavior that 52 | excludes people in socially marginalized groups. 53 | 54 | Private harassment is also unacceptable. No matter who you are, if you feel 55 | you have been or are being harassed or made uncomfortable by a community 56 | member, please contact [me](https://github.com/mathieudutour) 57 | immediately. Whether you're a regular contributor or a newcomer, we care about 58 | making this community a safe place for you and we've got your back. 59 | 60 | Likewise any spamming, trolling, flaming, baiting or other attention-stealing 61 | behavior is not welcome. 62 | 63 | 64 | # Communication 65 | 66 | GitHub issues are the primary way for communicating about specific proposed 67 | changes to this project. 68 | 69 | Please follow the conduct guidelines above. Language issues 70 | are often contentious and we'd like to keep discussion brief, civil and focused 71 | on what we're actually doing, not wandering off into too much imaginary stuff. 72 | 73 | # Frequently Asked Questions 74 | 75 | See [the FAQ docs page](/docs/FAQ.md) 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build artefacts 2 | Git.sketchplugin 3 | 4 | # npm 5 | node_modules 6 | 7 | # Logs 8 | logs 9 | *.log 10 | npm-debug.log* 11 | 12 | # Optional npm cache directory 13 | .npm 14 | -------------------------------------------------------------------------------- /.gitsketchrc: -------------------------------------------------------------------------------- 1 | { 2 | "exportFolder": ".exportedArtboards", 3 | "exportFormat": "png", 4 | "exportScale": "0.5", 5 | "includeOverviewFile": false, 6 | "autoExportOnSave": false 7 | } 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [0.11.3] - 2017-10-16 2 | 3 | * Fix NSArray error which blocked committing and generating files for diffs. 4 | 5 | 6 | ## [0.11.2] - 2017-05-25 7 | 8 | * URL-encode artboard image paths in the overview markdown (Thanks @mattjbray) 9 | 10 | 11 | ## [0.11.1] - 2017-05-05 12 | 13 | * Check if we are in a git repo before exporting the artboards (Thanks @yuchuanxi) 14 | * Add an option to export the artboards on save (Thanks @yuchuanxi) 15 | 16 | 17 | ## [0.11.0] - 2017-05-05 18 | 19 | * Fix export artboards when file name contains special character 20 | * Use a single sketchignore for entire repo which includes subdirectories 21 | 22 | 23 | ## [0.10.1] - 2017-04-18 24 | 25 | * Use webpack to compile everything instead of rollup. Should bring more stability. 26 | * Fix path to `exportArtboard.sh` script 27 | 28 | 29 | ## [0.10.0] - 2017-04-11 30 | 31 | * move export config to `.gitsketchrc` so that everybody working 32 | on the same file will have the same settings 33 | * fix the default config which could be break sometimes 34 | * fix exporting artboards when there is a space in the path to Sketch 35 | 36 | 37 | ## [0.9.2] - 2017-04-06 38 | 39 | * Added support for multiple file formats (png, jpg, pdf, eps, svg) 40 | for the pretty diff images (thanls @grrtbrtr) 41 | 42 | 43 | ## [0.9.1] - 2017-02-12 44 | 45 | * Update web-view dependency 46 | 47 | 48 | ## [0.9.0] - 2017-01-15 49 | 50 | * Add an overview md file next to each sketch file (thanks @philschatz) 51 | * Improve UI to manage preferences 52 | 53 | 54 | ## [0.8.5] - 2017-01-14 55 | 56 | * Fix typos causing a bunch of commands to fail (silly me) (thanks @philschatz) 57 | 58 | 59 | ## [0.8.4] - 2017-01-08 60 | 61 | * Add UI to manage branches 62 | * Make the plugin compatible with Sketch Runner 63 | 64 | 65 | ## [0.8.3] - 2016-12-01 66 | 67 | * Fix typo in analytics 68 | * Update build tools 69 | 70 | 71 | ## [0.8.2] - 2016-11-29 72 | 73 | * Fix a typo causing the fail alert not to show up 74 | * Optionally send anonymous usage data in order to improve the plugin 75 | 76 | 77 | ## [0.8.1] - 2016-11-25 78 | 79 | * More robust check for failure when executing a task 80 | 81 | 82 | ## [0.8.0] - 2016-11-25 83 | 84 | * Complete rewrite of the plugin using `sketch-builder` 85 | * Export symbols as well 86 | 87 | 88 | ## [0.7.4] - 2016-11-21 89 | 90 | * More lines for Commit: the first line is the commit message, all the others are an optional longer description 91 | 92 | 93 | ## [0.7.3] - 2016-10-03 94 | 95 | * fix non-ascii characters encoding (thanks @tomonari-t) 96 | 97 | 98 | ## [0.7.2] - 2016-09-08 99 | 100 | * fix when the path to the plugin has multiple spaces 101 | 102 | 103 | ## [0.7.1] - 2016-09-01 104 | 105 | * check if a file is open and send a helpful message if not 106 | * add a button to report the issue when one happens 107 | 108 | 109 | ## [0.7.0] - 2016-08-31 110 | 111 | * add option to exclude entire pages from pretty diffs 112 | 113 | 114 | ## [0.6.0] - 2016-08-27 115 | 116 | * check for new updates automatically 117 | 118 | 119 | ## [0.5.0] - 2016-08-27 120 | 121 | * add option to exclude artboards from pretty diffs 122 | 123 | * create a file called .sketchignore next to your sketchfiles 124 | * put the name of the artboards inside (see example). It can either be the exact name of the artboard or a regex 125 | * profit 126 | 127 | 128 | ## [0.4.0] - 2016-08-24 129 | 130 | * keep old generated artboards if not changed 131 | * add preference to control the scale of the exported artboards 132 | 133 | 134 | ## [0.3.5] - 2016-06-23 135 | 136 | * escape double quote in commit message 137 | 138 | 139 | ## [0.3.4] - 2016-05-30 140 | 141 | * fix missing argument in the export artboards function 142 | 143 | 144 | ## [0.3.3] - 2016-05-30 145 | 146 | * fix missing argument in the shared functions 147 | 148 | 149 | ## [0.3.2] - 2016-05-29 150 | 151 | * factorize cd into the current folder for every command 152 | * use `git for-each-ref` instead of `git branch + awk` to list the branches 153 | 154 | 155 | ## [0.3.1] - 2016-05-23 156 | 157 | * add preferences for the terminal to use 158 | * prefix user preferences by `gitSketch` to not conflict with others potentially 159 | * replace option of `push` (was `simple`, now `current`) 160 | 161 | 162 | ## [0.3.0] - 2016-05-21 163 | 164 | * add plugin preferences panel 165 | * add icon on alerts 166 | * open terminal with `iTerm` if available 167 | * add `pull` command 168 | * add default option for push 169 | * add popup to ask for the remote repository url 170 | 171 | 172 | ## [0.2.3] - 2016-04-18 173 | 174 | * use sketchtool from the bundle 175 | 176 | 177 | ## [0.2.2] - 2016-03-28 178 | 179 | * use user bah profile when running commands 180 | 181 | 182 | ## [0.2.1] - 2016-02-05 183 | 184 | * add checkbox to generate pretty diffs when commiting 185 | 186 | 187 | ## [0.2.0] - 2016-01-23 188 | 189 | * use sketchtool to generate pretty diffs 190 | * add command to add file to git 191 | 192 | 193 | ## [0.1.0] - 2016-01-22 194 | 195 | * first release 196 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Mathieu Dutour 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## This plugin was a first attempt to bring version control and collaboration to designers. Since then, Sketch evolved a lot and my new project takes advantages of them to unlock _true_ version control. Check it out: [http://kactus.io](http://kactus.io) 2 | 3 | --- 4 | 5 | 6 | 7 | # git-sketch-plugin 8 | 9 | [![GitHub release](https://img.shields.io/github/release/mathieudutour/git-sketch-plugin.svg?maxAge=2592000)](https://github.com/mathieudutour/git-sketch-plugin/releases) 10 | [![GitHub release](https://img.shields.io/badge/Works%20with-Sketch%20Runner-blue.svg?colorB=308ADF)](http://bit.ly/SketchRunnerWebsite) 11 | 12 | A Git client built right into [Sketch](http://www.bohemiancoding.com/sketch). Generate [pretty diffs](https://github.com/mathieudutour/git-sketch-plugin/pull/1/files) so that everybody knows what are the changes! 13 | 14 | From ... 15 | ![Ugly](example/ScreenShotBad.png) 16 | 17 | ... To 18 | ![Pretty](example/ScreenShotNice.png) 19 | 20 | ![screen cast](example/ScreenCast.gif) 21 | 22 | ## Requirements 23 | 24 | - [Sketch](http://sketchapp.com/) >= 3.4 (**not** with the sandboxed version ie from the App Store). 25 | - [Git](https://git-scm.com/) (coming with OS X so you shouldn't have to do anything) 26 | - [Xcode Command Line Tools](http://osxdaily.com/2014/02/12/install-command-line-tools-mac-os-x/) 27 | 28 | ## Installation 29 | 30 | ### From a release (simplest) 31 | 32 | - [Download](https://github.com/mathieudutour/git-sketch-plugin/releases/latest) the latest release of the plugin 33 | - Un-zip 34 | - Double-click on Git.sketchplugin 35 | 36 | ### From the sources 37 | 38 | - Clone the repo 39 | - Install the dependencies (`npm install`) 40 | - Build (`npm run build`) 41 | - Double-click on Git.sketchplugin 42 | 43 | ## Documentation 44 | 45 | For a Getting started guide, FAQ, etc. check out our [docs](https://github.com/mathieudutour/git-sketch-plugin/tree/master/docs)! 46 | 47 | ## Want to contribute? 48 | 49 | Anyone can help make this project better - check out our [Contributing guide](/.github/CONTRIBUTING.md)! 50 | -------------------------------------------------------------------------------- /Resources/branches.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: -apple-system; 3 | background: white; 4 | padding-top: 20px; 5 | } 6 | 7 | .branch { 8 | opacity: 0.6; 9 | height: 25px; 10 | clear: both; 11 | } 12 | 13 | .branch.selected { 14 | opacity: 1; 15 | } 16 | 17 | .branch:hover { 18 | opacity: 1; 19 | } 20 | 21 | .name { 22 | cursor: pointer; 23 | } 24 | 25 | .delete { 26 | float: right; 27 | cursor: pointer; 28 | } 29 | .delete img { 30 | width: 15px; 31 | } 32 | 33 | .create { 34 | position: fixed; 35 | width: 100%; 36 | bottom: 0; 37 | left: 0; 38 | border: 0; 39 | border-radius: 0; 40 | height: 40px; 41 | background-image: linear-gradient(180deg, #8f779d, #613b75); 42 | color: white; 43 | letter-spacing: 0.3px; 44 | font-size: 13px; 45 | cursor: pointer; 46 | z-index: 1; 47 | } 48 | 49 | #container { 50 | margin-bottom: 70px; 51 | } 52 | -------------------------------------------------------------------------------- /Resources/branches.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Branches 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Resources/branches.js: -------------------------------------------------------------------------------- 1 | import { h, render, Component } from "preact"; 2 | 3 | function cleanBranchName(name) { 4 | return name ? name.replace("(B", "") : name; 5 | } 6 | 7 | class Branch extends Component { 8 | render({ name, selected }) { 9 | return ( 10 |
11 | window.postMessage("checkoutBranch", name)} 14 | title="Switch to the branch" 15 | > 16 | {name} 17 | 18 | window.postMessage("deleteBranch", name)} 21 | > 22 | 23 | 24 |
25 | ); 26 | } 27 | } 28 | 29 | class Branches extends Component { 30 | constructor(props) { 31 | super(props); 32 | this.state = { 33 | branches: (window.branches || []).filter(x => x).map(cleanBranchName), 34 | currentBranch: cleanBranchName(window.currentBranch), 35 | ready: window.ready 36 | }; 37 | if (!window.ready) { 38 | const interval = setInterval(() => { 39 | if (window.ready) { 40 | this.setState({ 41 | branches: (window.branches || []) 42 | .filter(x => x) 43 | .map(cleanBranchName), 44 | currentBranch: cleanBranchName(window.currentBranch), 45 | ready: window.ready 46 | }); 47 | clearInterval(interval); 48 | } 49 | }, 100); 50 | } 51 | } 52 | 53 | render(props, { ready, branches, currentBranch }) { 54 | return ( 55 |
56 | 62 | {!ready && "loading..."} 63 | {(branches || []).map(name => ( 64 | 65 | ))} 66 |
67 | ); 68 | } 69 | } 70 | 71 | render(, document.getElementById("container")); 72 | -------------------------------------------------------------------------------- /Resources/preferences.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: -apple-system; 3 | background: white; 4 | padding-top: 10px; 5 | } 6 | 7 | .save { 8 | position: fixed; 9 | width: 100%; 10 | bottom: 0; 11 | left: 0; 12 | border: 0; 13 | border-radius: 0; 14 | height: 40px; 15 | background-image: linear-gradient(180deg, #8f779d, #613b75); 16 | color: white; 17 | letter-spacing: 0.3px; 18 | font-size: 13px; 19 | cursor: pointer; 20 | z-index: 1; 21 | } 22 | 23 | #container { 24 | margin-bottom: 70px; 25 | } 26 | 27 | h2 { 28 | margin-bottom: 0; 29 | } 30 | 31 | input[type="text"] { 32 | display: block; 33 | } 34 | 35 | .form { 36 | padding-top: 10px; 37 | } 38 | -------------------------------------------------------------------------------- /Resources/preferences.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Preferences 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Resources/preferences.js: -------------------------------------------------------------------------------- 1 | import { h, render, Component } from "preact"; 2 | import linkState from "linkstate"; 3 | 4 | class Preferences extends Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { 8 | preferences: window.preferences || {}, 9 | ready: window.ready 10 | }; 11 | if (!window.ready) { 12 | const interval = setInterval(() => { 13 | if (window.ready) { 14 | this.setState({ 15 | preferences: window.preferences || {}, 16 | ready: window.ready 17 | }); 18 | clearInterval(interval); 19 | } 20 | }, 100); 21 | } 22 | } 23 | 24 | render(props, { ready, preferences }) { 25 | return ( 26 |
27 | 33 | {!ready && "loading..."} 34 |

Diffs preferences

35 |
36 | 39 | 45 |
46 |
47 | 48 | 54 |
55 |
56 | 57 | 68 |
69 |
70 | 76 | 80 |
81 |
82 | 88 | 92 |
93 |
94 | 100 | 104 |
105 |

Miscellaneous

106 |
107 | 108 | 116 |
117 |
118 | ); 119 | } 120 | } 121 | 122 | render(, document.getElementById("container")); 123 | -------------------------------------------------------------------------------- /assets/delete.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathieudutour/git-sketch-plugin/a3f3564179833506e62a142220e60db1ca0ebef7/assets/icon.png -------------------------------------------------------------------------------- /assets/icons/branches.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathieudutour/git-sketch-plugin/a3f3564179833506e62a142220e60db1ca0ebef7/assets/icons/branches.png -------------------------------------------------------------------------------- /assets/icons/commit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathieudutour/git-sketch-plugin/a3f3564179833506e62a142220e60db1ca0ebef7/assets/icons/commit.png -------------------------------------------------------------------------------- /assets/icons/pull.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathieudutour/git-sketch-plugin/a3f3564179833506e62a142220e60db1ca0ebef7/assets/icons/pull.png -------------------------------------------------------------------------------- /assets/icons/push.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathieudutour/git-sketch-plugin/a3f3564179833506e62a142220e60db1ca0ebef7/assets/icons/push.png -------------------------------------------------------------------------------- /docs/FAQ.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | ## [I installed the plugin but it's not showing in the Sketch plugin menu?](https://github.com/mathieudutour/git-sketch-plugin/issues/77) 4 | Make sure that you downloaded the plugin from the releases page: https://github.com/mathieudutour/git-sketch-plugin/releases. 5 | 6 | ## What exactly are the pretty diffs? 7 | The plugin is generating one png per artboard, independently of wether they are in the same page or not. Just be careful if some of your artboards have the same name (only the last one will be generated). 8 | Those generated images are in a hidden folder and Github will pick them up to show the changes between commits ([example](https://github.com/mathieudutour/git-sketch-plugin/pull/1/files)) 9 | 10 | ## [Can two people work on the same file at the same time?](https://github.com/mathieudutour/git-sketch-plugin/issues/42) 11 | 12 | Because the sketch format is a binary, there is no way to know what has changed. Hence it is not possible to merge the changes made on the same file. 13 | 14 | The only advice I can give would be to work on smaller files where the concern are separate. 15 | 16 | ## I get an error when trying to generate the pretty diffs, what do I do? 17 | 18 | Make sure that: 19 | * [all used fonts are installed](https://github.com/mathieudutour/git-sketch-plugin/issues/14) 20 | 21 | If you still have an issue, [open a new one](https://github.com/mathieudutour/git-sketch-plugin/issues/new). 22 | 23 | ## I get `xcrun error: cannot be used within an App Sandbox.` whenever I try to use the plugin, what do I do? 24 | 25 | The plugin doesn't work with the app from the Mac App Store. It needs to access git and the file system which is impossible on a sandboxed app. 26 | 27 | ## I get `Failed... gpg: cannot open '/dev/tty': Device not configured` when I try to commit, what do I do? 28 | 29 | You are signing your commits with PGP/GPG, which is great! Unfortunately, you just hit a [common issue with GnuPG](https://github.com/Microsoft/vscode/issues/5065) (cf. #93). As a workaround, you can tell GnuPG to never 30 | use the `TTY`: 31 | 32 | $ echo 'no-tty' >> ~/.gnupg/gpg.conf 33 | 34 | More information about this option: 35 | 36 | > --no-tty 37 | > Make sure that the TTY (terminal) is never used for any output. This option is needed in some cases because 38 | > GnuPG sometimes prints ?? warnings to the TTY even if --batch is used. 39 | 40 | Note: this option will **not** disable GPG signed commits. Nonetheless, `gpg` will be slightly less verbose. 41 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # documentation 2 | 3 | * [Getting Started](getting-started.md) - How to get going with the git sketch plugin 4 | * [Ignoring artboards](sketchignore.md) - How not to generate the pretty diffs for some artboards 5 | * [Keyboard Shortcut](keyboard-shortcut.md) - How not to waste time in menus 6 | * [Enabling Git LFS](git-lfs.md) - How to maintain a healthy git repository 7 | * [Git commands](git-commands.md) - _advanced_ - What `git` commands is the plugin using behind the scene 8 | 9 | 10 | ## FAQ 11 | 12 | See the [FAQ](FAQ.md) for the answers to commonly asked questions. 13 | 14 | 15 | ## Examples 16 | 17 | - [simple example](https://github.com/mathieudutour/git-sketch-plugin/tree/master/example) 18 | -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | # Workflow 2 | Once: 3 | * Init the git repo (`Plugins > Git > Advanced > Init git repo`) 4 | * Add all the sketch files you are working on the repo (`Plugins > Git > Advanced > Add file to git`) 5 | 6 | Then: 7 | * Create a new branch when you start working on a new feature (`Plugins > Git > Branches`) 8 | * Work normally on your design 9 | * Save the file 10 | * Commit the changes with a meaningful message describing them. The plugin will extract the artboards in your file in order to show the differences easily. (`Plugins > Git > Commit`) 11 | * Push your changes to the remote. (`Plugins > Git > Push`) 12 | * Create a pull request from your branch to the master branch. 13 | * Voila. Your co-workers can review the changes, comment on them and approve them. Once approved, merge the pull request. 14 | 15 | For a more in-depth explanation of a nice git flow, check out [this article](https://about.gitlab.com/2014/09/29/gitlab-flow/). 16 | 17 | # In-depth explanation 18 | - git is a **distributed** version control system. It means that your repository (folder) live in different places at the same time. One of those places is your computer (local directory), another one is the github servers (remote). You can have others as well but it's beyond the scope of this discussion. 19 | 20 | To tell your computer that your folder should be tracked using git: `Plugins > Git > Advanced > Init git repo`. 21 | 22 | At this point you will be asked for the URL of the remote. Go to github, create a new repo and copy paste the URL of your repository in the plugin. Make sure you are using the correct format (SSH or HTTPS) according to your authentication method. 23 | 24 | 25 | - The different places are not sync automatically (if you would want that, which you don't believe me), use dropbox. Git allows you to sync your data only when you want to. 26 | 27 | 28 | - The first "way" to sync your data is when you have made changes on your local directory and that you want to **push** them to the remote (github in this case). You first have to **commit** your changes. When you commit your changes, you are saying to git: "Ok, I like what I have done now, take a snapshot of my files and store it". You need to enter a message to describe the changes you've made. This will allow your co-workers (and yourself) to remember what happened at that point. 29 | 30 | You can then push. To do that, you need to connect to your remote (github). Because not everybody can push on your repo, you need to tell git about your github credentials so that github knows that it's you. 31 | 32 | https://help.github.com/articles/set-up-git/ 33 | 34 | https://help.github.com/articles/caching-your-github-password-in-git/ 35 | 36 | - The second way to sync your data is when someone else has made some changes and pushed them to the remote. You now need to **pull** the changes from the remote to your local directory. 37 | -------------------------------------------------------------------------------- /docs/git-commands.md: -------------------------------------------------------------------------------- 1 | # Git commands used behind the scene 2 | 3 | Client | Command 4 | :----------------------------|:------------------------------------------ 5 | Commit | `git commit -m 'message' -a` 6 | Push | `git -c push.default=current push -q` 7 | New Branch | `git checkout -qb branchName` 8 | Switch Branch | `git checkout -q branchName` 9 | Pull | `git pull` 10 | Add file to git | `git add currentFile` 11 | Init Git repo | `git init && git add currentFile` 12 | -------------------------------------------------------------------------------- /docs/git-lfs.md: -------------------------------------------------------------------------------- 1 | # Enabling Git LFS _(optional)_ 2 | 3 | Git LFS is an extension for git that enables a quicker way to push and pull changes that involve binary files, like `.png` and `.sketch`. It is not critical to using this plugin, but it may help maintain a healthy git repository. 4 | 5 | _Note: In using the Git LFS extension, all contributors must have it installed, and the git remote (ie Github, Bitbucket, Gitlab) must have it enabled with sufficient storage available to the hosting account._ 6 | 7 | Start by enabling LFS for your repo: 8 | 9 | 1. [Downloading and install](https://git-lfs.github.com/) Git LFS to your machine 10 | 2. Go into the terminal for your repo (through Sketch, `Plugins > Git > Advanced > Open terminal`) 11 | 3. Paste the following into your repo, `git lfs install && git lfs track '*.png' && git lfs track '*.sketch' && git add .gitattributes` 12 | 4. Thats it. You need only run these commands once. Your team mates will have to download and install Git LFS onto their machines as well (so, just step 1). 13 | -------------------------------------------------------------------------------- /docs/keyboard-shortcut.md: -------------------------------------------------------------------------------- 1 | # Key bindings 2 | 3 | Action | Shortcut 4 | :-----------------------------|:--------------------------------------- 5 | Commit your changes | ctrl + alt + cmd + c 6 | Push your changes | ctrl + alt + cmd + p 7 | Create a new branch | ctrl + alt + cmd + n 8 | Switch to an existing branch | ctrl + alt + cmd + o 9 | -------------------------------------------------------------------------------- /docs/sketchignore.md: -------------------------------------------------------------------------------- 1 | # Ignoring artboards for the pretty diffs 2 | 3 | * create a file called `.sketchignore` next to your sketch files 4 | * put the name of the artboards inside (see [example](https://github.com/mathieudutour/git-sketch-plugin/blob/master/example/.sketchignore)). 5 | It can either be the exact name of the artboard or a regex to match multiple artboards at once. 6 | * profit 7 | -------------------------------------------------------------------------------- /example/.exportedArtboards/example/Artboard 1@0.5x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathieudutour/git-sketch-plugin/a3f3564179833506e62a142220e60db1ca0ebef7/example/.exportedArtboards/example/Artboard 1@0.5x.png -------------------------------------------------------------------------------- /example/.exportedArtboards/example/Rectangle@0.5x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathieudutour/git-sketch-plugin/a3f3564179833506e62a142220e60db1ca0ebef7/example/.exportedArtboards/example/Rectangle@0.5x.png -------------------------------------------------------------------------------- /example/.sketchignore: -------------------------------------------------------------------------------- 1 | Page 1/ignored 2 | 3 | Page 1/ignored-regex-(.*) 4 | 5 | Ignored page/(.*) 6 | -------------------------------------------------------------------------------- /example/ScreenCast.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathieudutour/git-sketch-plugin/a3f3564179833506e62a142220e60db1ca0ebef7/example/ScreenCast.gif -------------------------------------------------------------------------------- /example/ScreenShotBad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathieudutour/git-sketch-plugin/a3f3564179833506e62a142220e60db1ca0ebef7/example/ScreenShotBad.png -------------------------------------------------------------------------------- /example/ScreenShotNice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathieudutour/git-sketch-plugin/a3f3564179833506e62a142220e60db1ca0ebef7/example/ScreenShotNice.png -------------------------------------------------------------------------------- /example/example-boards.md: -------------------------------------------------------------------------------- 1 | # Artboards 2 | 3 | This is an autogenerated file showing all the artboards. Do not edit it directly. 4 | 5 | ## Artboard 1@0.5x 6 | 7 | ![Artboard 1@0.5x](./.exportedArtboards/example/Artboard 1@0.5x.png) 8 | 9 | 10 | ## Rectangle@0.5x 11 | 12 | ![Rectangle@0.5x](./.exportedArtboards/example/Rectangle@0.5x.png) 13 | 14 | -------------------------------------------------------------------------------- /example/example.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathieudutour/git-sketch-plugin/a3f3564179833506e62a142220e60db1ca0ebef7/example/example.sketch -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathieudutour/git-sketch-plugin/a3f3564179833506e62a142220e60db1ca0ebef7/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "git-sketch-plugin", 3 | "version": "0.12.1", 4 | "description": "Plugin to handle versioning in git", 5 | "main": "Git.sketchplugin", 6 | "skpm": { 7 | "name": "Git", 8 | "manifest": "src/manifest.json", 9 | "identifier": "me.dutour.mathieu.git-plugin", 10 | "assets": [ 11 | "assets/**/*" 12 | ], 13 | "resources": [ 14 | "Resources/branches.js", 15 | "Resources/preferences.js" 16 | ] 17 | }, 18 | "directories": { 19 | "doc": "docs", 20 | "example": "example" 21 | }, 22 | "scripts": { 23 | "test": "eslint -c ./.eslintrc src/ && eslint -c ./.eslintrc Resources/", 24 | "postinstall": "npm run build && skpm-link", 25 | "build": "skpm-build", 26 | "watch": "skpm-build --watch", 27 | "publish": "skpm publish" 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "git+https://github.com/mathieudutour/git-sketch-plugin.git" 32 | }, 33 | "author": "Mathieu Dutour ", 34 | "license": "MIT", 35 | "bugs": { 36 | "url": "https://github.com/mathieudutour/git-sketch-plugin/issues" 37 | }, 38 | "homepage": "https://github.com/mathieudutour/git-sketch-plugin#readme", 39 | "devDependencies": { 40 | "@skpm/builder": "^0.7.5", 41 | "@skpm/extract-loader": "^2.0.2", 42 | "babel-plugin-transform-react-jsx": "^6.8.0", 43 | "css-loader": "^3.4.2", 44 | "eslint": "^6.8.0", 45 | "eslint-config-prettier": "^6.10.1", 46 | "eslint-config-sketch": "^0.2.4", 47 | "eslint-config-standard": "^14.1.1", 48 | "eslint-config-standard-preact": "^1.0.1", 49 | "eslint-plugin-import": "^2.20.2", 50 | "eslint-plugin-node": "^11.1.0", 51 | "eslint-plugin-prettier": "^3.1.2", 52 | "eslint-plugin-promise": "^4.2.1", 53 | "eslint-plugin-react": "^7.19.0", 54 | "eslint-plugin-standard": "^4.0.1", 55 | "html-loader": "^0.5.5", 56 | "prettier": "^2.0.2" 57 | }, 58 | "dependencies": { 59 | "@skpm/child_process": "^0.4.2", 60 | "@skpm/dialog": "^0.4.0", 61 | "@skpm/fs": "^0.2.6", 62 | "linkstate": "^1.1.1", 63 | "preact": "^10.3.4", 64 | "sketch-module-user-preferences": "^1.0.1", 65 | "sketch-module-web-view": "^3.4.1" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/commands/Add.js: -------------------------------------------------------------------------------- 1 | // Add this file to the repo 2 | import { 3 | getCurrentFileName, 4 | checkForFile, 5 | executeSafely, 6 | exec, 7 | } from "../common"; 8 | import { UI } from "sketch"; 9 | 10 | export default function () { 11 | if (!checkForFile()) { 12 | return; 13 | } 14 | executeSafely(function () { 15 | const currentFileName = getCurrentFileName(); 16 | if (currentFileName) { 17 | exec(`git add "${currentFileName}"`); 18 | UI.message("File added to git"); 19 | } 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /src/commands/Branches.js: -------------------------------------------------------------------------------- 1 | // Branches (cmd alt ctrl b) 2 | import { UI } from "sketch"; 3 | import { getCurrentBranch, checkForFile, exec, executeSafely } from "../common"; 4 | import WebUI from "sketch-module-web-view"; 5 | 6 | export default function () { 7 | if (!checkForFile()) { 8 | return; 9 | } 10 | executeSafely(() => { 11 | let listBranches = exec( 12 | "git for-each-ref --format='%(refname:short)' refs/heads/" 13 | ); 14 | if (!listBranches) { 15 | UI.message("No branches"); 16 | return; 17 | } 18 | 19 | listBranches = listBranches.split("\n"); 20 | const currentBranch = getCurrentBranch(); 21 | const webUI = new WebUI({ 22 | identifier: "git-sketch-plugin.branches", 23 | height: 280, 24 | width: 250, 25 | resizable: false, 26 | minimizable: false, 27 | maximizable: false, 28 | titleBarStyle: "hidden", 29 | show: false, 30 | }); 31 | 32 | webUI.loadURL(require("../../Resources/branches.html")); 33 | 34 | webUI.once("ready-to-show", () => { 35 | webUI.show(); 36 | webUI.webContents.executeJavaScript( 37 | 'window.branches=["' + listBranches.join('", "') + '"]' 38 | ); 39 | webUI.webContents.executeJavaScript( 40 | 'window.currentBranch="' + currentBranch + '"' 41 | ); 42 | webUI.webContents.executeJavaScript("window.ready=true"); 43 | }); 44 | 45 | webUI.webContents.on("checkoutBranch", (name) => { 46 | executeSafely(function () { 47 | exec(`git checkout -q ${name}`); 48 | const app = NSApp.delegate(); 49 | app.refreshCurrentDocument(); 50 | webUI.close(); 51 | UI.message(`Switched to branch '${name}'`); 52 | }); 53 | }); 54 | webUI.webContents.on("deleteBranch", (name) => { 55 | executeSafely(function () { 56 | exec(`git branch -d ${name}`); 57 | UI.message(`Deleted branch '${name}'`); 58 | }); 59 | }); 60 | webUI.webContents.on("createBranch", () => { 61 | executeSafely(function () { 62 | UI.getInputFromUser( 63 | "New Branch Name", 64 | { okButton: "Create Branch" }, 65 | (err, value) => { 66 | if (err) { 67 | return; 68 | } 69 | exec(`git checkout -qb ${value.trim()}`); 70 | UI.message(`Switched to a new branch '${value.trim()}'`); 71 | webUI.close(); 72 | } 73 | ); 74 | }); 75 | }); 76 | }); 77 | } 78 | -------------------------------------------------------------------------------- /src/commands/Commit.js: -------------------------------------------------------------------------------- 1 | // Commits all working file to git (cmd alt ctrl c) 2 | import { UI } from "sketch"; 3 | import { 4 | getCurrentBranch, 5 | checkForFile, 6 | executeSafely, 7 | exec, 8 | createInputWithCheckbox, 9 | } from "../common"; 10 | import { exportArtboards } from "../exportArtboards"; 11 | import { getUserPreferences } from "../preferences"; 12 | 13 | export default function () { 14 | if (!checkForFile()) { 15 | return; 16 | } 17 | executeSafely(function () { 18 | const currentBranch = getCurrentBranch(); 19 | const prefs = getUserPreferences(); 20 | const commitMsg = createInputWithCheckbox( 21 | 'Commit to "' + currentBranch + '"', 22 | "Generate files for pretty diffs", 23 | prefs.diffByDefault, 24 | "Commit" 25 | ); 26 | 27 | if (commitMsg.responseCode == 1000 && commitMsg.message != null) { 28 | if (commitMsg.checked) { 29 | exportArtboards(prefs); 30 | } 31 | const command = `git commit -m "${commitMsg.message 32 | .split('"') 33 | .join('\\"')}" -a; exit`; 34 | const message = exec(command); 35 | UI.message(message.split("\n").join(" ")); 36 | } 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /src/commands/Export.js: -------------------------------------------------------------------------------- 1 | // Export artboards for pretty diffs 2 | import { UI } from "sketch"; 3 | import { checkForFile, checkForGitRepository, executeSafely } from "../common"; 4 | import { exportArtboards } from "../exportArtboards"; 5 | import { getUserPreferences } from "../preferences"; 6 | 7 | export default function () { 8 | if (!checkForFile() && !checkForGitRepository()) { 9 | return; 10 | } 11 | executeSafely(function () { 12 | exportArtboards(getUserPreferences()); 13 | UI.message("Artboards exported"); 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /src/commands/Init.js: -------------------------------------------------------------------------------- 1 | // Init git repo and add current file to the repo (cmd alt ctrl n) 2 | import { UI } from "sketch"; 3 | import { 4 | checkForFile, 5 | getCurrentFileName, 6 | executeSafely, 7 | exec, 8 | createFailAlert, 9 | } from "../common"; 10 | 11 | export default function () { 12 | if (!checkForFile()) { 13 | return; 14 | } 15 | executeSafely(function () { 16 | const currentFileName = getCurrentFileName(); 17 | if (!currentFileName) { 18 | createFailAlert("Failed...", "Cannot get the current file name"); 19 | return; 20 | } 21 | 22 | const message = exec(`git init && git add "${currentFileName}"`); 23 | UI.message(message); 24 | UI.getInputFromUser( 25 | "URL of the remote repo", 26 | { 27 | okButton: "Add Remote", 28 | cancelButton: "Not now", 29 | description: "you can create one here: https://github.com/new", 30 | }, 31 | (err, value) => { 32 | if (err) { 33 | return; 34 | } 35 | const res = exec(`git remote add origin ${value.trim()}; exit`); 36 | UI.message(res.split("\n").join(" ")); 37 | } 38 | ); 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /src/commands/OpenTerminal.js: -------------------------------------------------------------------------------- 1 | // Opens terminal in working directory (cmd alt ctrl o) 2 | import { getCurrentDirectory } from "../common"; 3 | import { getUserPreferences } from "../preferences"; 4 | 5 | export default function () { 6 | const path = getCurrentDirectory(); 7 | const { terminal } = getUserPreferences(); 8 | NSWorkspace.sharedWorkspace().openFile_withApplication_(path, terminal); 9 | } 10 | -------------------------------------------------------------------------------- /src/commands/Preferences.js: -------------------------------------------------------------------------------- 1 | // Commits all working file to git (cmd alt ctrl c) 2 | import { UI } from "sketch"; 3 | import WebUI from "sketch-module-web-view"; 4 | import { executeSafely } from "../common"; 5 | import { getUserPreferences, setUserPreferences } from "../preferences"; 6 | 7 | export default function () { 8 | const preferences = getUserPreferences(); 9 | const webUI = new WebUI({ 10 | identifier: "git-sketch-plugin.preferences", 11 | width: 340, 12 | height: 400, 13 | resizable: false, 14 | minimizable: false, 15 | maximizable: false, 16 | titleBarStyle: "hidden", 17 | show: false, 18 | }); 19 | 20 | webUI.loadURL(require("../../Resources/preferences.html")); 21 | 22 | webUI.once("ready-to-show", () => { 23 | webUI.show(); 24 | webUI.webContents.executeJavaScript( 25 | "window.preferences=" + JSON.stringify(preferences) 26 | ); 27 | webUI.webContents.executeJavaScript("window.ready=true"); 28 | }); 29 | 30 | webUI.webContents.on("savePreferences", (prefs) => { 31 | executeSafely(function () { 32 | setUserPreferences(prefs); 33 | webUI.close(); 34 | UI.message("Preferences updated"); 35 | }); 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /src/commands/Pull.js: -------------------------------------------------------------------------------- 1 | // Pull 2 | import { UI } from "sketch"; 3 | import { checkForFile, executeSafely, exec } from "../common"; 4 | 5 | export default function () { 6 | if (!checkForFile()) { 7 | return; 8 | } 9 | executeSafely(function () { 10 | exec("git pull -q"); 11 | UI.message("Changes pulled"); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /src/commands/Push.js: -------------------------------------------------------------------------------- 1 | // Push (cmd alt ctrl p) 2 | import { UI } from "sketch"; 3 | import { checkForFile, executeSafely, exec } from "../common"; 4 | 5 | export default function () { 6 | if (!checkForFile()) { 7 | return; 8 | } 9 | executeSafely(function () { 10 | exec("git -c push.default=current push -q"); 11 | UI.message("Changes pushed"); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /src/commands/autoExportOnSave.js: -------------------------------------------------------------------------------- 1 | // Export artboards for pretty diffs when document saved 2 | import { UI } from "sketch"; 3 | import { checkForGitRepository, executeSafely } from "../common"; 4 | import { exportArtboards } from "../exportArtboards"; 5 | import { getUserPreferences } from "../preferences"; 6 | 7 | export default function () { 8 | const prefs = getUserPreferences(); 9 | 10 | if (!prefs.autoExportOnSave || !checkForGitRepository()) { 11 | return; 12 | } 13 | executeSafely(() => { 14 | exportArtboards(prefs); 15 | UI.message("Artboards exported"); 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /src/common.js: -------------------------------------------------------------------------------- 1 | // Common library of things 2 | import { Document } from "sketch"; 3 | import { execSync } from "@skpm/child_process"; 4 | import dialog from "@skpm/dialog"; 5 | 6 | function getPluginAlertIcon() { 7 | /* eslint-disable */ 8 | if (__command.pluginBundle() && __command.pluginBundle().alertIcon()) { 9 | return __command.pluginBundle().alertIcon(); 10 | } 11 | /* eslint-enable */ 12 | return NSImage.imageNamed("plugins"); 13 | } 14 | 15 | export function executeSafely(func) { 16 | try { 17 | func(); 18 | } catch (e) { 19 | createFailAlert("Failed...", e, true); 20 | } 21 | } 22 | 23 | export function exec(command) { 24 | const path = getCurrentDirectory(); 25 | command = `cd "${path}" && ${command}`; 26 | 27 | return execSync(command, { 28 | cwd: path, 29 | shell: "/bin/bash", 30 | encoding: "utf8", 31 | }).trim(); 32 | } 33 | 34 | export function getCurrentDirectory() { 35 | const document = Document.getSelectedDocument(); 36 | return String( 37 | document.sketchObject.fileURL().URLByDeletingLastPathComponent().path() 38 | ); 39 | } 40 | 41 | export function getGitDirectory() { 42 | return exec("git rev-parse --show-toplevel").trim().replace("(B", ""); 43 | } 44 | 45 | export function getCurrentFileName() { 46 | const document = Document.getSelectedDocument(); 47 | return String(document.sketchObject.fileURL().lastPathComponent()); 48 | } 49 | 50 | export function createFailAlert(title, error, buttonToReport) { 51 | console.log(error); 52 | const responseCode = dialog.showMessageBoxSync({ 53 | type: "error", 54 | message: title, 55 | detail: "" + error, 56 | buttons: ["OK", ...(buttonToReport ? ["Report Issue"] : [])], 57 | }); 58 | 59 | if (responseCode) { 60 | let errorString = error; 61 | if (typeof error === "object") { 62 | try { 63 | errorString = JSON.stringify(error, null, "\t"); 64 | if (errorString === "{}") { 65 | errorString = error; 66 | } 67 | } catch (e) {} 68 | } 69 | if (error && error.message) { 70 | errorString = `${error.message}\n\n${errorString}`; 71 | } 72 | if (error && error.stack) { 73 | errorString = `${errorString}\n\n${error.stack}`; 74 | } 75 | const urlString = `https://github.com/mathieudutour/git-sketch-plugin/issues/new?body=${encodeURIComponent( 76 | "### How did it happen?\n1.\n2.\n3.\n\n\n### Error log\n\n```\n" + 77 | errorString + 78 | "\n```" 79 | )}`; 80 | const url = NSURL.URLWithString(urlString); 81 | NSWorkspace.sharedWorkspace().openURL(url); 82 | } 83 | 84 | return { 85 | responseCode, 86 | }; 87 | } 88 | 89 | export function createInputWithCheckbox( 90 | msg, 91 | checkboxMsg, 92 | checked, 93 | okLabel, 94 | cancelLabel 95 | ) { 96 | const accessory = NSView.alloc().initWithFrame(NSMakeRect(0, 0, 300, 100)); 97 | const input = TextArea(0, 25, 300, 75); 98 | const checkbox = NSButton.alloc().initWithFrame(NSMakeRect(0, 0, 300, 25)); 99 | checkbox.setButtonType(3); 100 | checkbox.title = checkboxMsg; 101 | checkbox.state = checked ? 1 : 0; 102 | accessory.addSubview(input.view); 103 | accessory.addSubview(checkbox); 104 | 105 | const alert = NSAlert.alloc().init(); 106 | alert.setMessageText(msg); 107 | alert.addButtonWithTitle(okLabel || "OK"); 108 | alert.addButtonWithTitle(cancelLabel || "Cancel"); 109 | alert.setIcon(getPluginAlertIcon()); 110 | alert.setAccessoryView(accessory); 111 | 112 | const responseCode = alert.runModal(); 113 | const message = input.getValue(); 114 | 115 | return { 116 | responseCode: responseCode, 117 | message: String(message), 118 | checked: checkbox.state() == 1, 119 | }; 120 | } 121 | 122 | export function getCurrentBranch() { 123 | const path = getCurrentDirectory(); 124 | const currentBranchCommand = `cd "${path}" && git rev-parse --abbrev-ref HEAD`; 125 | let branch; 126 | try { 127 | branch = exec(currentBranchCommand).split("\n")[0]; 128 | } catch (e) { 129 | branch = "master"; 130 | } 131 | return branch; 132 | } 133 | 134 | export function checkForFile() { 135 | try { 136 | getCurrentFileName(); 137 | getCurrentDirectory(); 138 | return true; 139 | } catch (e) { 140 | createFailAlert( 141 | "Missing file", 142 | "You need to open a sketch file before doing that" 143 | ); 144 | return false; 145 | } 146 | } 147 | export function checkForGitRepository() { 148 | try { 149 | getGitDirectory(); 150 | return true; 151 | } catch (e) { 152 | createFailAlert( 153 | "Not a git repository", 154 | "You need to init git repository first" 155 | ); 156 | return false; 157 | } 158 | } 159 | 160 | function TextArea(x, y, width, heigh) { 161 | const scrollView = NSScrollView.alloc().initWithFrame( 162 | NSMakeRect(x, y, width, heigh) 163 | ); 164 | scrollView.borderStyle = NSLineBorder; 165 | const contentSize = scrollView.contentSize(); 166 | const input = NSTextView.alloc().initWithFrame( 167 | NSMakeRect(0, 0, contentSize.width, contentSize.height) 168 | ); 169 | input.minSize = NSMakeSize(0, contentSize.height); 170 | input.maxSize = NSMakeSize(contentSize.width, Infinity); 171 | scrollView.documentView = input; 172 | return { 173 | view: scrollView, 174 | getValue: () => input.string(), 175 | }; 176 | } 177 | -------------------------------------------------------------------------------- /src/exportArtboards.js: -------------------------------------------------------------------------------- 1 | import fs from "@skpm/fs"; 2 | import path from "path"; 3 | import { getCurrentFileName, getCurrentDirectory, exec } from "./common"; 4 | 5 | function findUpward(fileName, currentDirectory) { 6 | if (fs.existsSync(path.join(currentDirectory, fileName))) { 7 | return fs.readFileSync(path.join(currentDirectory, fileName), "utf8"); 8 | } 9 | if ( 10 | !currentDirectory || 11 | currentDirectory === "/" || 12 | currentDirectory === "." 13 | ) { 14 | return ""; 15 | } 16 | return findUpward(fileName, path.dirname(currentDirectory)); 17 | } 18 | 19 | export function exportArtboards(prefs) { 20 | const currentFileName = getCurrentFileName(); 21 | const currentDirectory = getCurrentDirectory(); 22 | const currentFileNameWithoutExtension = currentFileName.replace( 23 | /\.sketch$/, 24 | "" 25 | ); 26 | const { 27 | exportFolder, 28 | exportFormat, 29 | exportScale, 30 | includeOverviewFile, 31 | } = prefs; 32 | const bundlePath = NSBundle.mainBundle().bundlePath(); 33 | const fileFolder = path.join( 34 | currentDirectory, 35 | exportFolder, 36 | currentFileNameWithoutExtension 37 | ); 38 | 39 | // get list of artboards regex to ignore 40 | const sketchIgnore = findUpward(".sketchignore", currentDirectory) 41 | .split("\n") 42 | .filter((x) => x.trim()) 43 | .map((x) => new RegExp(x)); 44 | 45 | // get list of artboard names to export 46 | const artboards = []; 47 | const sketchtoolOutput = JSON.parse( 48 | exec( 49 | `"${bundlePath}/Contents/Resources/sketchtool/bin/sketchtool" list artboards "${currentFileName}" --include-symbols=YES` 50 | ) 51 | ); 52 | sketchtoolOutput.pages.forEach((page) => { 53 | page.artboards.forEach((artboard) => { 54 | const name = page.name + "/" + artboard.name; 55 | if (sketchIgnore.every((regex) => !regex.test(name))) { 56 | artboards.push(artboard.name); 57 | } 58 | }); 59 | }); 60 | 61 | try { 62 | fs.mkdirSync(exportFolder, { recursive: true }); 63 | } catch (err) { 64 | // ignore 65 | } 66 | 67 | // move old artboards to temp directory to compare them with the new ones 68 | try { 69 | fs.rmdirSync(path.join(currentDirectory, ".oldArtboards")); 70 | } catch (err) { 71 | // ignore 72 | } 73 | try { 74 | fs.renameSync(fileFolder, path.join(currentDirectory, ".oldArtboards")); 75 | } catch (err) { 76 | // ignore 77 | } 78 | try { 79 | fs.rmdirSync(fileFolder); 80 | fs.unlinkSync(fileFolder); 81 | } catch (err) { 82 | // ignore 83 | } 84 | 85 | console.log(artboards); 86 | 87 | // generate new artboards 88 | fs.mkdirSync(fileFolder, { recursive: true }); 89 | console.log( 90 | `"${bundlePath}/Contents/Resources/sketchtool/bin/sketchtool" export artboards "${currentFileName}" --formats="${ 91 | exportFormat || "png" 92 | }" --scales="${exportScale}" --output="${fileFolder}" --overwriting=YES --items="${artboards.join( 93 | "," 94 | )}" --include-symbols=YES` 95 | ); 96 | exec( 97 | `"${bundlePath}/Contents/Resources/sketchtool/bin/sketchtool" export artboards "${currentFileName}" --formats="${ 98 | exportFormat || "png" 99 | }" --scales="${exportScale}" --output="${fileFolder}" --overwriting=YES --items="${artboards.join( 100 | "," 101 | )}" --include-symbols=YES` 102 | ); 103 | 104 | // Construct a ${FILENAME}-boards.md file which shows all the artboards in the sketch directory 105 | const readmeFile = path.join( 106 | currentDirectory, 107 | `./${currentFileName.replace(".sketch", "")}-boards.md` 108 | ); // Exclude the file extension 109 | if (includeOverviewFile) { 110 | fs.writeFileSync( 111 | readmeFile, 112 | `# Artboards 113 | 114 | This is an autogenerated file showing all the artboards. Do not edit it directly.` 115 | ); 116 | } 117 | 118 | // compare new artboards with the old ones 119 | fs.readdirSync(fileFolder).forEach((artboard) => { 120 | if (fs.existsSync(path.join(currentDirectory, ".oldArtboards", artboard))) { 121 | const newArtboard = fs.readFileSync(path.join(fileFolder, artboard)); 122 | const oldArtboard = fs.readFileSync( 123 | path.join(currentDirectory, ".oldArtboards", artboard) 124 | ); 125 | 126 | if (newArtboard.equals(oldArtboard)) { 127 | // keep the old artboard 128 | fs.unlinkSync(path.join(fileFolder, artboard)); 129 | fs.renameSync( 130 | path.join(currentDirectory, ".oldArtboards", artboard), 131 | path.join(fileFolder, artboard) 132 | ); 133 | } 134 | } 135 | 136 | if (includeOverviewFile) { 137 | const artboardName = artboard.replace(path.extname(artboard), ""); // Exclude the file extension 138 | const artboardPathUrlEncoded = encodeURIComponent( 139 | `${exportFolder}/${currentFileNameWithoutExtension}/${artboard}` 140 | ); 141 | fs.appendFileSync( 142 | readmeFile, 143 | ` 144 | ## ${artboardName} 145 | 146 | ![${artboardName}](./${artboardPathUrlEncoded}) 147 | ` 148 | ); 149 | } 150 | }); 151 | 152 | exec(`git add "${exportFolder}"`); 153 | 154 | if (includeOverviewFile) { 155 | exec(`git add "${readmeFile}"`); 156 | } 157 | 158 | try { 159 | fs.rmdirSync(path.join(currentDirectory, ".oldArtboards")); 160 | } catch (err) { 161 | // ignore 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "icon": "icon.png", 3 | "commands": [ 4 | { 5 | "name": "autoExportOnSave", 6 | "identifier": "autoExportOnSave", 7 | "script": "./commands/autoExportOnSave.js", 8 | "handlers": { 9 | "actions": { 10 | "DocumentSaved": "onRun" 11 | } 12 | } 13 | }, 14 | { 15 | "name": "Commit", 16 | "identifier": "commit", 17 | "shortcut": "cmd alt ctrl c", 18 | "script": "./commands/Commit.js", 19 | "description": "Commit your changes.", 20 | "icon": "icons/commit.png" 21 | }, 22 | { 23 | "name": "Push", 24 | "identifier": "push", 25 | "shortcut": "cmd alt ctrl p", 26 | "script": "./commands/Push.js", 27 | "description": "Push your commits to GitHub.", 28 | "icon": "icons/push.png" 29 | }, 30 | { 31 | "name": "Branches", 32 | "identifier": "branches", 33 | "shortcut": "cmd alt ctrl b", 34 | "script": "./commands/Branches.js", 35 | "description": "Manage git branches.", 36 | "icon": "icons/branches.png" 37 | }, 38 | { 39 | "name": "Pull", 40 | "identifier": "pull", 41 | "script": "./commands/Pull.js", 42 | "description": "Pull changes and branches from GitHub.", 43 | "icon": "icons/pull.png" 44 | }, 45 | { 46 | "name": "Generate files for pretty diffs", 47 | "identifier": "export", 48 | "script": "./commands/Export.js" 49 | }, 50 | { 51 | "name": "Add file to git", 52 | "identifier": "add", 53 | "script": "./commands/Add.js" 54 | }, 55 | { 56 | "name": "Init git repo", 57 | "identifier": "init", 58 | "script": "./commands/Init.js" 59 | }, 60 | { 61 | "name": "Preferences", 62 | "identifier": "preferences", 63 | "script": "./commands/Preferences.js" 64 | }, 65 | { 66 | "name": "Open terminal", 67 | "identifier": "terminal", 68 | "script": "./commands/OpenTerminal.js" 69 | } 70 | ], 71 | "menu": { 72 | "title": "Git", 73 | "items": [ 74 | "commit", 75 | "push", 76 | "branches", 77 | "pull", 78 | { 79 | "title": "Advanced", 80 | "items": ["init", "export", "add", "terminal"] 81 | }, 82 | "preferences" 83 | ] 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/preferences.js: -------------------------------------------------------------------------------- 1 | import prefsManager from "sketch-module-user-preferences"; 2 | import fs from "@skpm/fs"; 3 | import { exec, getGitDirectory } from "./common"; 4 | 5 | const keyPref = "gitSketch"; 6 | const PREFS_FILE = ".gitsketchrc"; 7 | const LOCAL_PREFS = { 8 | exportFolder: ".exportedArtboards", 9 | exportFormat: "png", 10 | exportScale: "1.0", 11 | includeOverviewFile: true, 12 | autoExportOnSave: false, 13 | }; 14 | const GLOBAL_PREFS = { 15 | terminal: "Terminal", 16 | diffByDefault: true, 17 | }; 18 | 19 | export function getUserPreferences() { 20 | let localPrefs = {}; 21 | try { 22 | const path = getGitDirectory(); 23 | localPrefs = JSON.parse(fs.readFileSync(path + "/" + PREFS_FILE)); 24 | } catch (e) { 25 | console.log(e); 26 | } 27 | return Object.assign( 28 | {}, 29 | LOCAL_PREFS, 30 | prefsManager.getUserPreferences(keyPref, GLOBAL_PREFS), 31 | localPrefs 32 | ); 33 | } 34 | 35 | export function setUserPreferences(prefs) { 36 | const localPrefs = {}; 37 | const globalPrefs = {}; 38 | Object.keys(prefs).forEach((k) => { 39 | if (Object.keys(LOCAL_PREFS).indexOf(k) !== -1) { 40 | localPrefs[k] = prefs[k]; 41 | } else { 42 | globalPrefs[k] = prefs[k]; 43 | } 44 | }); 45 | 46 | try { 47 | const path = getGitDirectory(); 48 | fs.writeFileSync( 49 | path + "/" + PREFS_FILE, 50 | JSON.stringify(localPrefs, null, " ") 51 | ); 52 | exec('git add "' + path + "/" + PREFS_FILE + '"'); 53 | } catch (e) { 54 | console.log(e); 55 | } 56 | return prefsManager.setUserPreferences(keyPref, globalPrefs); 57 | } 58 | -------------------------------------------------------------------------------- /webpack.skpm.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.module.rules.push({ 3 | test: /\.(html)$/, 4 | use: [ 5 | { 6 | loader: "@skpm/extract-loader" 7 | }, 8 | { 9 | loader: "html-loader", 10 | options: { 11 | attrs: ["img:src", "link:href"], 12 | interpolate: true 13 | } 14 | } 15 | ] 16 | }); 17 | config.module.rules.push({ 18 | test: /\.(css)$/, 19 | use: [ 20 | { 21 | loader: "@skpm/extract-loader" 22 | }, 23 | { 24 | loader: "css-loader" 25 | } 26 | ] 27 | }); 28 | }; 29 | --------------------------------------------------------------------------------