├── .gitignore ├── .npmignore ├── LICENSE.md ├── README.md ├── lib ├── local-history-view.js ├── local-history.js ├── purge.js └── utils.js ├── menus └── local-history.cson ├── package.json ├── spec └── local-history-spec.js └── styles └── local-history.less /.gitignore: -------------------------------------------------------------------------------- 1 | /*.log 2 | node_modules 3 | /.Trash-1000/ 4 | .DS_Store 5 | Thumbs.db 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /.Trash-1000/ 3 | .DS_Store 4 | Thumbs.db 5 | *.log 6 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | local-history - https://github.com/Nicolab/atom-local-history 2 | 3 | The MIT License (MIT) 4 | 5 | Copyright (c) 2014 Nicolas Tallefourtane dev@nicolab.net 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining 8 | a copy of this software and associated documentation files (the 9 | "Software"), to deal in the Software without restriction, including 10 | without limitation the rights to use, copy, modify, merge, publish, 11 | distribute, sublicense, and/or sell copies of the Software, and to 12 | permit persons to whom the Software is furnished to do so, subject to 13 | the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 22 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 24 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Local History for Atom 2 | 3 | Atom package for maintaining a local history of files (history of your changes to the code files). 4 | 5 | > Need a new maintainer: I switched to VScode editor, I do not use Atom anymore. Because of this, it is not practical for me to maintain this package. 6 | 7 | ## Why? 8 | 9 | For maintaining a history of the files revisions like mostly code editors: 10 | * ["Local History" of Eclipse IDE](http://help.eclipse.org/juno/index.jsp?topic=%2Forg.eclipse.platform.doc.user%2Freference%2Fref-6a.htm) 11 | * [The plugin "Local History" for SublimeText](https://github.com/vishr/local-history) 12 | * IntelliJ and other... 13 | 14 | > Especially, I need an automated backup of my work to save me from stupid accidents... :fist: 15 | 16 | 17 | ## Benefits 18 | 19 | * Local history of a file is maintained when you create or edit a file. 20 | Each time you save a file, a copy of the old contents is kept in the local history. 21 | * It can help you out when you change or delete a file by accident. 22 | * It saves your work when you forget to commit or push your code. 23 | * The history can also help you out when your workspace has a catastrophic problem 24 | or if you get disk errors that corrupt your workspace files. 25 | * Each file revision is stored in a separate file (with full path) inside the `.atom/local-history` directory of your home directory. 26 | e.g: `/home/nicolas/.atom/local-history/var/www/my-great-project/lib/2014-06-21_17-05-43_utils.js` 27 | * Show diff and merge with your favorite diff tool. 28 | 29 | 30 | ## Install 31 | 32 | ```sh 33 | apm install local-history 34 | ``` 35 | Or Settings ➔ Packages ➔ Search for `local-history` 36 | 37 | ## Usage 38 | 39 | Show the history of the current file: 40 | 41 | * From the context menu (right click) 42 | 43 | ![Contextual menu](http://i.imgur.com/HNeP768.png) 44 | 45 | 46 | * Or with the command 47 | 48 | ![Commands](http://i.imgur.com/3UAfYHo.png) 49 | 50 | 51 | Then, select the revision to open in another tab 52 | 53 | ![Revisions list](http://i.imgur.com/x14qm5n.png) 54 | 55 | 56 | ### command 57 | 58 | * `local-history:current-file` show local history of current file. 59 | * `local-history:difftool-current-file` Open the current file and a given revision file with your defined diff tool (see [difftoolCommand](#difftoolcommand)). 60 | * `local-history:purge` purge the expired revisions (see [daysLimit](#dayslimit)). 61 | 62 | 63 | ## Settings 64 | 65 | ### historyStoragePath 66 | 67 | Path where the revision files are stored. 68 | By default in your Atom (home) directory : _.atom/local-history_. 69 | 70 | ### AutoPurge 71 | 72 | Automatic purge (triggered, max 1 time per day). 73 | 74 | Disabled by default. 75 | You must check to enable this option. 76 | 77 | ### fileSizeLimit 78 | 79 | File size limit, by default: 262144 (256 KB). 80 | The files heavier than the defined size will not be saved. 81 | 82 | 83 | ### daysLimit 84 | 85 | Days retention limit, by default: 30 days by file. 86 | The oldest files are deleted when purging (local-history:purge). 87 | 88 | 89 | ### difftoolCommand 90 | 91 | A custom command to open your favorite diff tool, by default: [meld](http://meldmerge.org). 92 | 93 | Example: 94 | 95 | ```sh 96 | meld "{current-file}" "{revision-file}" 97 | ``` 98 | * `{current-file}` is the placeholder replaced automatically by the path of the current file. 99 | * `{revision-file}` is the placeholder replaced automatically by the path of the revision file selected. 100 | 101 | The actual command generated will be something like this: 102 | ```sh 103 | meld "/var/www/my-project/my-current-file.js" "/home/nicolas/.atom/local-history/var/www/my-project/2014-07-08_19-32-00_my-current-file.js" 104 | ``` 105 | 106 | ### difftoolCommandShowErrorMessage 107 | 108 | Show the errors of the diff tool command. 109 | If checked, the errors are displayed in a message panel. 110 | 111 | Enabled by default. 112 | 113 | 114 | ## LICENSE 115 | 116 | [MIT](https://github.com/Nicolab/atom-local-history/blob/master/LICENSE.md) (c) 2013, Nicolas Tallefourtane. 117 | 118 | 119 | ## Author 120 | 121 | | [![Nicolas Tallefourtane - Nicolab.net](http://www.gravatar.com/avatar/d7dd0f4769f3aa48a3ecb308f0b457fc?s=64)](http://nicolab.net) | 122 | |---| 123 | | [Nicolas Talle](http://nicolab.net) | 124 | | [![Make a donation via Paypal](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=PGRH4ZXP36GUC) | 125 | -------------------------------------------------------------------------------- /lib/local-history-view.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @name local-history (view) 5 | * @author Nicolas Tallefourtane 6 | * @link https://github.com/Nicolab/atom-local-history 7 | * @license MIT https://github.com/Nicolab/atom-local-history/blob/master/LICENSE.md 8 | */ 9 | 10 | const helpers = require('atom-helpers'); 11 | const SelectListView = require('atom-space-pen-views').SelectListView; 12 | const exec = require("child_process").exec; 13 | const path = require('path'); 14 | const fsPlus = require('fs-plus'); 15 | const humanize = require('humanize-plus'); 16 | const utils = require('./utils'); 17 | const purge = require('./purge'); 18 | const messagePanel = require('atom-message-panel'); 19 | const MessagePanelView = messagePanel.MessagePanelView; 20 | const PlainMessageView = messagePanel.PlainMessageView; 21 | const localHistoryPath = utils.getLocalHistoryPath(); 22 | 23 | let fileSizeLimit; 24 | 25 | /** 26 | * The view. 27 | */ 28 | class LocalHistoryView extends SelectListView { 29 | constructor() { 30 | super(...arguments); 31 | this.currentCommand = null; 32 | this.purge = purge; 33 | } 34 | 35 | initialize() { 36 | super.initialize(...arguments); 37 | 38 | fileSizeLimit = atom.config.get('local-history.fileSizeLimit'); 39 | 40 | if (atom.config.get('local-history.autoPurge')) { 41 | let lastPurgeTime = localStorage.getItem('localHistory.lastPurge'); 42 | let time = (new Date()).getTime() / 1000; 43 | 44 | if (!lastPurgeTime || (time - lastPurgeTime) > 86400) { 45 | localStorage.setItem('localHistory.lastPurge', time); 46 | 47 | setTimeout(() => { 48 | atom.commands.dispatch( 49 | atom.views.getView(atom.workspace), 50 | 'local-history:purge' 51 | ); 52 | }, 5000); 53 | } 54 | } 55 | 56 | return this; 57 | } 58 | 59 | destroy() { 60 | this.currentCommand = null; 61 | 62 | return super.detach(...arguments); 63 | } 64 | 65 | cancelled() { 66 | this.hide(); 67 | } 68 | 69 | hide() { 70 | this.modalPanel.hide(); 71 | } 72 | 73 | viewForItem(item) { 74 | let ext = path.extname(item); 75 | let typeClass = 'icon-file-text'; 76 | 77 | if (fsPlus.isReadmePath(item)) { 78 | typeClass = 'icon-book'; 79 | } 80 | else if (fsPlus.isCompressedExtension(ext)) { 81 | typeClass = 'icon-file-zip'; 82 | } 83 | else if (fsPlus.isImageExtension(ext)) { 84 | typeClass = 'icon-file-media'; 85 | } 86 | else if (fsPlus.isPdfExtension(ext)) { 87 | typeClass = 'icon-file-pdf'; 88 | } 89 | else if (fsPlus.isBinaryExtension(ext)) { 90 | typeClass = 'icon-file-binary'; 91 | } 92 | 93 | return '
  • ' + 94 | '
    ' + 96 | utils.getFileDate(item) + ' - ' + utils.getOriginBaseName(item) + 97 | '
    ' + 98 | 99 | '
    ' + 100 | item.substr(localHistoryPath.length) + 101 | '
    ' + 102 | '
  • '; 103 | } 104 | 105 | confirmed(item) { 106 | let fileSize, messages; 107 | 108 | fileSize = fsPlus.getSizeSync(item); 109 | 110 | if (fileSizeLimit < fileSize) { 111 | messages = new MessagePanelView({ 112 | title: 'Local history: file size limit', 113 | rawTitle: true 114 | }); 115 | 116 | messages.attach(); 117 | 118 | messages.add(new PlainMessageView({ 119 | message: 'The size of the selected file (' + humanize.fileSize(fileSize) + 120 | ') is larger than the value of your configuration (' + 121 | humanize.fileSize(fileSize) + ').
    ' + 122 | 'If you want, you can open directly the file ' + item + '', 123 | raw: true 124 | })); 125 | 126 | return this; 127 | } 128 | 129 | if (this.currentCommand === 'difftool-current-file') { 130 | this.openDifftoolForCurrentFile(item); 131 | } 132 | else if (this.currentCommand === 'current-file'){ 133 | atom.workspace.open(item); 134 | } 135 | 136 | return this; 137 | } 138 | 139 | findLocalHistory() { 140 | let currentFilePath, needRevList, workspaceView; 141 | 142 | workspaceView = atom.views.getView(atom.workspace); 143 | currentFilePath = helpers.editor.getCurrentFilePath(); 144 | 145 | needRevList = [ 146 | 'current-file', 147 | 'difftool-current-file' 148 | ] 149 | .indexOf(this.currentCommand) !== -1; 150 | 151 | this.addClass('local-history overlay from-top'); 152 | 153 | if (needRevList && currentFilePath) { 154 | this.setItems(utils.getFileRevisionList(currentFilePath)); 155 | } 156 | 157 | if (!this.has('.local-history-path').length) { 158 | this.append('
    ' + 159 | 'Local history path: ' + localHistoryPath + '
    '); 160 | } 161 | 162 | this.modalPanel = atom.workspace.addModalPanel({item: this}); 163 | this.modalPanel.show(); 164 | 165 | this.focusFilterEditor(); 166 | } 167 | 168 | openDifftoolForCurrentFile(revision) { 169 | let currentFilePath = helpers.editor.getCurrentFilePath(); 170 | 171 | if (currentFilePath) { 172 | return this.openDifftool(currentFilePath, revision); 173 | } 174 | } 175 | 176 | openDifftool(current, revision) { 177 | let basePath = atom.project.getPaths()[0]; 178 | let diffCmd = atom.config.get('local-history.difftoolCommand'); 179 | 180 | if (diffCmd && basePath && current && revision) { 181 | diffCmd = diffCmd 182 | .replace(/\{current-file\}/g, current) 183 | .replace(/\{revision-file\}/g, revision) 184 | ; 185 | 186 | // If command starts with protocol (eg compare-files:), then we try to open it with atom 187 | if (diffCmd.match(/^([a-zA-Z0-9\-_]+:)/)) { 188 | return atom.workspace.open(diffCmd, { searchAllPanes : true }); 189 | } 190 | 191 | return exec('cd "' + basePath + '" && ' + diffCmd, function(err) { 192 | let messages; 193 | 194 | if (!err) { 195 | return; 196 | } 197 | 198 | console.warn('local-history: difftool error', err); 199 | console.debug('local-history: difftool command', diffCmd); 200 | 201 | if (atom.config.get('local-history.difftoolCommandShowErrorMessage')) { 202 | messages = new MessagePanelView({ 203 | title: 'Local history: difftool Command', 204 | rawTitle: true 205 | }); 206 | 207 | messages.attach(); 208 | 209 | messages.add(new PlainMessageView({ 210 | message: 'Check the field value of difftool Command ' + 211 | 'in your settings (local-history package).' + 212 | '
    Command error: ' + diffCmd + '' + 213 | '

    Error details: ' + err.toString(), 214 | raw: true 215 | })); 216 | } 217 | }); 218 | } 219 | } 220 | 221 | saveRevision(buffer) { 222 | let file, revFileName; 223 | 224 | let now = new Date(); 225 | let day = '' + now.getDate(); 226 | let month = '' + (now.getMonth() + 1); 227 | let hour = '' + now.getHours(); //24-hours format 228 | let minute = '' + now.getMinutes(); 229 | let second = '' + now.getSeconds(); 230 | let pathDirName = path.dirname(buffer.file.path); 231 | 232 | if (day.length === 1) { 233 | day = '0' + day; 234 | } 235 | 236 | if (month.length === 1) { 237 | month = '0' + month; 238 | } 239 | 240 | if (hour.length === 1) { 241 | hour = '0' + hour; 242 | } 243 | 244 | if (minute.length === 1) { 245 | minute = '0' + minute; 246 | } 247 | 248 | if (second.length === 1) { 249 | second = '0' + second; 250 | } 251 | 252 | // YYYY-mm-dd_HH-ii-ss_basename 253 | revFileName = now.getFullYear() + 254 | '-' + month + 255 | '-' + day + 256 | '_' + hour + 257 | '-' + minute + 258 | '-' + second + 259 | '_' + path.basename(buffer.file.path) 260 | ; 261 | 262 | if (process.platform === 'win32') { 263 | pathDirName = pathDirName.replace(/:/g,''); 264 | } 265 | 266 | file = path.join(localHistoryPath, pathDirName, revFileName); 267 | 268 | fsPlus.writeFile(file, buffer.getText(), function(err) { 269 | let messages; 270 | 271 | if (err) { 272 | messages = new MessagePanelView({ 273 | title: 'Local history: write error', 274 | rawTitle: true 275 | }); 276 | 277 | messages.attach(); 278 | 279 | messages.add(new PlainMessageView({ 280 | message: '- Can not save the revision of the file: ' + 281 | buffer.file.path + '
    ' + 282 | '- Revision file is not saved: ' + file + '' + 283 | '
    - ' + err.toString() + '
    ', 284 | raw: true 285 | })); 286 | } 287 | }); 288 | } 289 | } 290 | 291 | module.exports = LocalHistoryView; 292 | -------------------------------------------------------------------------------- /lib/local-history.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @name local-history (main) 5 | * @author Nicolas Tallefourtane 6 | * @link https://github.com/Nicolab/atom-local-history 7 | * @license MIT https://github.com/Nicolab/atom-local-history/blob/master/LICENSE.md 8 | */ 9 | const utils = require('./utils'); 10 | 11 | module.exports = { 12 | localHistoryView: null, 13 | 14 | config: { 15 | 16 | // 256 KB 17 | fileSizeLimit: { 18 | type: 'integer', 19 | default: 262144, 20 | description: 'Size max in byte. The files heavier than the defined size will not be saved.' 21 | }, 22 | 23 | // in days 24 | daysLimit: { 25 | type: 'integer', 26 | default: 30, 27 | description: 'Days retention limit by original files. ' 28 | + 'The oldest revision files are deleted when purging (local-history:purge)' 29 | }, 30 | 31 | // enable automatic purge 32 | autoPurge: { 33 | type: 'boolean', 34 | default: false, 35 | title: 'Automatic purge', 36 | description: 'Enable or Disable the automatic purge. Triggered, max 1 time per day.' 37 | }, 38 | 39 | historyStoragePath: { 40 | type: 'string', 41 | default: utils.getLocalHistoryPath(), 42 | title: 'History storage path.', 43 | description: 'Path where the revision files are stored.', 44 | }, 45 | 46 | difftoolCommand: { 47 | type: 'string', 48 | default: 'meld "{current-file}" "{revision-file}"', 49 | description: 'A custom command to open your favorite diff tool' 50 | }, 51 | 52 | // show error message in a message panel 53 | difftoolCommandShowErrorMessage: { 54 | type: 'boolean', 55 | default: true, 56 | title: 'Show the errors of the diff tool command', 57 | description: 'Display the errors in a message panel' 58 | } 59 | }, 60 | 61 | activate(state) { 62 | let _this, fsPlus, fileSizeLimit, workspaceView; 63 | 64 | _this = this; 65 | fsPlus = require('fs-plus'); 66 | fileSizeLimit = atom.config.get('local-history.fileSizeLimit'); 67 | workspaceView = atom.views.getView(atom.workspace); 68 | 69 | atom.workspace.observeTextEditors(function(editor) { 70 | editor.buffer.onWillSave(function(/*willSaved*/) { 71 | let buffer = editor.buffer; 72 | let hasFilePath = buffer && buffer.file && buffer.file.path; 73 | 74 | if (buffer.isModified() 75 | && hasFilePath 76 | && fsPlus.getSizeSync(buffer.file.path) < fileSizeLimit) { 77 | _this.getView().saveRevision(buffer); 78 | } 79 | }); 80 | }); 81 | 82 | atom.commands.add( 83 | 'atom-workspace', 84 | 'local-history:current-file', 85 | function() { 86 | let view = _this.getView(); 87 | 88 | view.currentCommand = 'current-file'; 89 | view.findLocalHistory(); 90 | } 91 | ); 92 | 93 | atom.commands.add( 94 | 'atom-workspace', 95 | 'local-history:difftool-current-file', 96 | function() { 97 | let view = _this.getView(); 98 | 99 | view.currentCommand = 'difftool-current-file'; 100 | view.findLocalHistory(); 101 | } 102 | ); 103 | 104 | atom.commands.add( 105 | 'atom-workspace', 106 | 'local-history:purge', 107 | function() { 108 | let view = _this.getView(); 109 | 110 | view.currentCommand = 'purge'; 111 | view.purge(); 112 | } 113 | ); 114 | }, 115 | 116 | deactivate() { 117 | return this.getView().destroy(); 118 | }, 119 | 120 | serialize() { 121 | return { 122 | localHistoryViewState: this.getView().serialize() 123 | }; 124 | }, 125 | 126 | getView() { 127 | if (!this.localHistoryView) { 128 | let LocalHistoryView = require('./local-history-view'); 129 | this.localHistoryView = new LocalHistoryView(); 130 | } 131 | 132 | return this.localHistoryView; 133 | } 134 | }; 135 | -------------------------------------------------------------------------------- /lib/purge.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @name local-history (purge) 5 | * @author Nicolas Tallefourtane 6 | * @link https://github.com/Nicolab/atom-local-history 7 | * @license MIT https://github.com/Nicolab/atom-local-history/blob/master/LICENSE.md 8 | */ 9 | 10 | const utils = require('./utils'); 11 | const promise = require("bluebird"); 12 | const fs = promise.promisifyAll(require("fs")); 13 | const path = require('path'); 14 | const messagePanel = require('atom-message-panel'); 15 | const MessagePanelView = messagePanel.MessagePanelView; 16 | const PlainMessageView = messagePanel.PlainMessageView; 17 | const localHistoryPath = utils.getLocalHistoryPath(); 18 | 19 | /** 20 | * Remove empty directories (asynchronous). 21 | * 22 | * @param {array} directories Array of directory paths to remove if empty. 23 | * @param {object} onError Error handler 24 | */ 25 | function removeEmptyDirectories(directories, onError) { 26 | for (let i in directories) { 27 | // if the directory is empty, it is removed 28 | fs.rmdir(directories[i], function(err) { 29 | if (err && err.code != 'ENOTEMPTY') { 30 | onError && onError(err); 31 | } 32 | }); 33 | } 34 | 35 | return true; 36 | } 37 | 38 | /** 39 | * Handle the flow of the purge. 40 | */ 41 | class Purge { 42 | /** 43 | * @constructor 44 | */ 45 | constructor() { 46 | this.files = {}; 47 | } 48 | 49 | /** 50 | * Error handler. 51 | * 52 | * @param {object} error Error object 53 | * @param {string} revFilePath Revision file path 54 | */ 55 | onError(error, revFilePath) { 56 | let messages, message; 57 | let report = {localHistoryPath, error}; 58 | 59 | messages = new MessagePanelView({ 60 | title: 'Local history: purge error', 61 | rawTitle: true 62 | }); 63 | 64 | message = '- Can not purge the local history: ' 65 | + localHistoryPath + '' 66 | ; 67 | 68 | if (revFilePath) { 69 | message += '
    - path: ' + revFilePath + ''; 70 | report.path = revFilePath; 71 | } 72 | 73 | message += '
    - ' + error.toString() + '
    '; 74 | 75 | messages.attach(); 76 | messages.add(new PlainMessageView({message, raw: true})); 77 | 78 | console.error('local-history:error-purge', report); 79 | } 80 | 81 | /** 82 | * Add a revision in the `files` container. 83 | * 84 | * @param {object} files Files container 85 | * @param {string} revFilePath Revision file path 86 | * @return {object} Returns `files` container with `revFilePath` added inside 87 | */ 88 | addRevFile(files, revFilePath) { 89 | let originBaseName = utils.getOriginBaseName(revFilePath); 90 | let key = path.join(path.dirname(revFilePath), originBaseName); 91 | let day = utils.getFileDate(revFilePath).substr(0, 10); 92 | 93 | if (!files[key]) { 94 | files[key] = { 95 | days: {} 96 | }; 97 | } 98 | 99 | if (!files[key].days[day]) { 100 | files[key].days[day] = []; 101 | } 102 | 103 | files[key].days[day].push(revFilePath); 104 | 105 | return files; 106 | } 107 | 108 | /** 109 | * Purge expired revisions (asynchronous). 110 | * 111 | * @param {object} files A container of revision `files`. 112 | * @return {Promise|number} The `Promise` of last removed file(s) number. 113 | * @see Purge.addRevFile() 114 | */ 115 | purgeRevFiles(files) { 116 | let daysLimit, lastRemoved, fileDays, days, day; 117 | 118 | daysLimit = atom.config.get('local-history.daysLimit'); 119 | lastRemoved = 0; 120 | 121 | for (let key in files) { 122 | fileDays = files[key].days; 123 | days = Object.keys(fileDays); 124 | 125 | if (days.length <= daysLimit) { 126 | continue; 127 | } 128 | 129 | days = days.sort().slice(0, days.length - daysLimit); 130 | 131 | for (let i in days) { 132 | day = days[i]; 133 | 134 | for (let iDay in fileDays[day]) { 135 | lastRemoved = fs 136 | .unlinkAsync(fileDays[day][iDay]) 137 | .then(() => lastRemoved + 1) 138 | .catch((err) => this.onError(err, fileDays[i])) 139 | ; 140 | } 141 | } 142 | 143 | if (lastRemoved === days.length) { 144 | throw new Error( 145 | 'Some files are not removed:
    expected: ' + days.length + 146 | ' / removed: ' + lastRemoved 147 | ); 148 | } 149 | } 150 | 151 | return lastRemoved; 152 | } 153 | 154 | /** 155 | * Scan a directory (recursive and asynchronous). 156 | * 157 | * @param {string} dirPath The path of directory to scan. 158 | * 159 | * @param {function} [onFile] The {function} to execute on each file, 160 | * receives a single argument the absolute path. 161 | * 162 | * @param {function} [onDirectory] The {function} to execute on each directory, 163 | * receives a single argument the absolute path. 164 | * 165 | * @return {Promise|array} Returns a `Promise` of an array of files / directories paths. 166 | */ 167 | scanDir(dirPath, onFile, onDirectory) { 168 | return fs 169 | .readdirAsync(dirPath) 170 | .map((fileName) => { 171 | let filePath = path.join(dirPath, fileName); 172 | 173 | return fs.statAsync(filePath).then((stat) => { 174 | if (stat.isDirectory()) { 175 | onDirectory && onDirectory(filePath); 176 | 177 | return this.scanDir(filePath, onFile, onDirectory); 178 | } 179 | 180 | onFile && onFile(filePath); 181 | 182 | return filePath; 183 | }); 184 | }) 185 | .reduce((filePaths, val) => filePaths.concat(val), []) 186 | ; 187 | } 188 | 189 | /** 190 | * Purge a directory (recursive and asynchronous). 191 | * 192 | * @param {string} dirPath See `Purge.scanDir()` 193 | * @param {function} [onFile] See `Purge.scanDir()` 194 | * @param {function} [onDirectory] See `Purge.scanDir()` 195 | * @return {Promise|number} See Purge.purgeRevFiles() 196 | */ 197 | purgeDir(dirPath, onFile, onDirectory) { 198 | return this 199 | .scanDir(dirPath, onFile, onDirectory) 200 | .then((filePaths) => { 201 | let files = {}; 202 | 203 | for (let i in filePaths) { 204 | files = this.addRevFile(files, filePaths[i]); 205 | } 206 | 207 | return files; 208 | }) 209 | .then((files) => this.purgeRevFiles(files)) 210 | ; 211 | } 212 | 213 | /** 214 | * Run the purge. 215 | * Purge expired revisions, then purge the empty directories. 216 | */ 217 | run() { 218 | let directories = []; 219 | 220 | console.log('local-history:purge start'); 221 | 222 | return this 223 | .purgeDir( 224 | localHistoryPath, 225 | null, 226 | (dirPath) => directories.push(dirPath) 227 | ) 228 | .then(() => removeEmptyDirectories(directories, this.onError)) 229 | .then(() => console.log('local-history:purge end')) 230 | .catch(this.onError) 231 | ; 232 | } 233 | } 234 | 235 | /** 236 | * Purge the old revisions files of local history. 237 | * 238 | * @see Purge.run() 239 | */ 240 | module.exports = function purge() { 241 | let p = new Purge(); 242 | 243 | p.run(); 244 | }; 245 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @name local-history (utils) 3 | * @author Nicolas Tallefourtane 4 | * @link https://github.com/Nicolab/atom-local-history 5 | * @license MIT https://github.com/Nicolab/atom-local-history/blob/master/LICENSE.md 6 | */ 7 | 8 | const fs = require('fs-plus'); 9 | const path = require('path'); 10 | 11 | let localHistoryPath = atom.config.get('local-history.historyStoragePath'); 12 | 13 | if (!localHistoryPath) { 14 | localHistoryPath = path.join(__dirname, '..', '..', '..', 'local-history'); 15 | } 16 | 17 | const utils = { 18 | normalizeFileName(filePath, defaultValue) { 19 | if (typeof filePath === 'string' && filePath.length) { 20 | return filePath; 21 | } 22 | 23 | if (typeof filePath === 'number') { 24 | return String(filePath); 25 | } 26 | 27 | return defaultValue; 28 | }, 29 | 30 | getFileDate(filePath) { 31 | let basePath = path.basename(filePath); 32 | let splitPath = basePath.split('_'); 33 | let date = splitPath[0]; 34 | let time = splitPath[1] ? splitPath[1].split('-') : ['00', '00', '00']; 35 | 36 | time = time[0] + ':' + time[1] + ':' + time[2]; 37 | return date + ' ' + time; 38 | }, 39 | 40 | getOriginBaseName(filePath) { 41 | if (this.normalizeFileName(filePath) === undefined) { 42 | return; 43 | } 44 | 45 | let basePath = path.basename(filePath); 46 | 47 | return basePath.substr(basePath.split('_', 2).join('_').length + 1); 48 | }, 49 | 50 | getFileRevisionList(filePath) { 51 | let isItsRev, originBaseName, files, fileBaseName, pathDirName, list; 52 | 53 | files = []; 54 | fileBaseName = path.basename(filePath); 55 | pathDirName = path.dirname(filePath); 56 | 57 | if (process.platform === 'win32') { 58 | pathDirName = pathDirName.replace(/:/g,''); 59 | } 60 | 61 | // list the directory (recursively) of the file 62 | list = fs.listTreeSync(path.join( 63 | this.getLocalHistoryPath(), 64 | pathDirName 65 | )); 66 | 67 | for (let i in list) { 68 | originBaseName = this.getOriginBaseName(list[i]); 69 | 70 | isItsRev = ( 71 | typeof originBaseName === 'string' 72 | && path.basename(originBaseName) === fileBaseName 73 | ); 74 | 75 | if (isItsRev && fs.isFileSync(list[i])) { 76 | files.push(list[i]); 77 | } 78 | } 79 | 80 | return files.sort().reverse(); 81 | }, 82 | 83 | getLocalHistoryPath() { 84 | return localHistoryPath; 85 | } 86 | }; 87 | 88 | module.exports = utils; 89 | -------------------------------------------------------------------------------- /menus/local-history.cson: -------------------------------------------------------------------------------- 1 | # See https://atom.io/docs/latest/creating-a-package#menus for more details 2 | 'context-menu': 3 | 'atom-text-editor': 4 | [ 5 | { 6 | 'label': 'Show history of current file', 7 | 'command': 'local-history:current-file' 8 | }, 9 | { 10 | 'label': 'Open difftool: current file / revision file', 11 | 'command': 'local-history:difftool-current-file' 12 | } 13 | ] 14 | 15 | 'menu': [ 16 | { 17 | 'label': 'Packages' 18 | 'submenu': [ 19 | 'label': 'Local history' 20 | 'submenu': [ 21 | { 22 | 'label': 'Show current file', 23 | 'command': 'local-history:current-file' 24 | }, 25 | { 26 | 'label': 'Open difftool: current file / revision file', 27 | 'command': 'local-history:difftool-current-file' 28 | }, 29 | { 30 | 'label': 'Purge expired revisions', 31 | 'command': 'local-history:purge' 32 | } 33 | ] 34 | ] 35 | } 36 | ] 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "local-history", 3 | "main": "./lib/local-history", 4 | "version": "4.3.1", 5 | "description": "Maintaining local history of files (history of your changes to the code files).", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/Nicolab/atom-local-history" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/Nicolab/atom-local-history/issues" 12 | }, 13 | "author": { 14 | "name": "Nicolas Tallefourtane", 15 | "url": "http://nicolab.net" 16 | }, 17 | "licenses": [ 18 | { 19 | "type": "MIT", 20 | "url": "https://github.com/Nicolab/atom-local-history/blob/master/LICENSE.md" 21 | } 22 | ], 23 | "engines": { 24 | "atom": ">0.192.0" 25 | }, 26 | "dependencies": { 27 | "atom-helpers": "^1.1.4", 28 | "atom-message-panel": "^1.2.2", 29 | "atom-space-pen-views": "^2.0.5", 30 | "bluebird": "^2.9.24", 31 | "fs-plus": "^2.7.0", 32 | "humanize-plus": "^1.5.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /spec/local-history-spec.js: -------------------------------------------------------------------------------- 1 | var WorkspaceView = require('atom').WorkspaceView; 2 | var LocalHistory = require('../lib/local-history'); 3 | 4 | describe('LocalHistory', function() { 5 | 6 | var activationPromise = null; 7 | 8 | beforeEach(function() { 9 | atom.workspaceView = new WorkspaceView(); 10 | activationPromise = atom.packages.activatePackage('local-history'); 11 | }); 12 | 13 | describe('when `local-history:current-file` event is triggered', function() { 14 | 15 | it('attaches the view', function() { 16 | 17 | expect(atom.workspaceView.find('.local-history')).not.toExist(); 18 | 19 | atom.workspaceView.trigger('local-history:current-file'); 20 | 21 | waitsForPromise(function() { 22 | return activationPromise; 23 | }); 24 | 25 | runs(function() { 26 | expect(atom.workspaceView.find('.local-history')).toExist(); 27 | expect(atom.workspaceView.find('.local-history-path')).toExist(); 28 | }); 29 | 30 | }); 31 | 32 | }); 33 | 34 | describe('when `local-history:difftool-current-file` event is triggered', function() { 35 | 36 | it('attaches the view', function() { 37 | 38 | expect(atom.workspaceView.find('.local-history')).not.toExist(); 39 | 40 | atom.workspaceView.trigger('local-history:difftool-current-file'); 41 | 42 | waitsForPromise(function() { 43 | return activationPromise; 44 | }); 45 | 46 | runs(function() { 47 | expect(atom.workspaceView.find('.local-history')).toExist(); 48 | expect(atom.workspaceView.find('.local-history-path')).toExist(); 49 | }); 50 | 51 | }); 52 | 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /styles/local-history.less: -------------------------------------------------------------------------------- 1 | .local-history { 2 | .local-history-path { 3 | margin-top: 5px; 4 | } 5 | } 6 | --------------------------------------------------------------------------------