├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── docs ├── announcement.md ├── conflict-area.jpg ├── conflict-resolution.gif ├── merge-complete.jpg ├── merge-progress.jpg ├── useful-test-cases.md └── were-done-here.jpg ├── keymaps └── merge-conflicts.cson ├── lib ├── atom_merge ├── conflict.js ├── conflicted-editor.coffee ├── git │ ├── common.js │ ├── gitops.js │ ├── index.js │ └── shellout.js ├── main.coffee ├── merge-state.coffee ├── mergetool.gitconfig ├── navigator.coffee ├── side.coffee └── view │ ├── covering-view.coffee │ ├── error-view.coffee │ ├── merge-conflicts-view.coffee │ ├── navigation-view.coffee │ ├── resolver-view.coffee │ └── side-view.coffee ├── menus └── merge-conflicts.cson ├── package.json ├── script └── test ├── spec ├── conflict-spec.coffee ├── conflicted-editor-spec.coffee ├── fixtures │ ├── corrupted-2way-diff.txt │ ├── corrupted-3way-diff.txt │ ├── irebasing.git │ │ ├── COMMIT_EDITMSG │ │ ├── HEAD │ │ ├── MERGE_MSG │ │ ├── ORIG_HEAD │ │ ├── config │ │ ├── description │ │ ├── hooks │ │ │ ├── applypatch-msg.sample │ │ │ ├── commit-msg.sample │ │ │ ├── post-update.sample │ │ │ ├── pre-applypatch.sample │ │ │ ├── pre-commit.sample │ │ │ ├── pre-push.sample │ │ │ ├── pre-rebase.sample │ │ │ ├── prepare-commit-msg.sample │ │ │ └── update.sample │ │ ├── index │ │ ├── info │ │ │ └── exclude │ │ ├── logs │ │ │ ├── HEAD │ │ │ └── refs │ │ │ │ └── heads │ │ │ │ ├── branch │ │ │ │ ├── master │ │ │ │ └── rebaser │ │ ├── rebase-merge │ │ │ ├── author-script │ │ │ ├── done │ │ │ ├── end │ │ │ ├── git-rebase-todo │ │ │ ├── git-rebase-todo.backup │ │ │ ├── head-name │ │ │ ├── interactive │ │ │ ├── message │ │ │ ├── msgnum │ │ │ ├── onto │ │ │ ├── orig-head │ │ │ ├── patch │ │ │ ├── quiet │ │ │ └── stopped-sha │ │ └── refs │ │ │ └── heads │ │ │ ├── branch │ │ │ ├── master │ │ │ └── rebaser │ ├── merging.git │ │ ├── COMMIT_EDITMSG │ │ ├── HEAD │ │ ├── MERGE_HEAD │ │ ├── MERGE_MODE │ │ ├── MERGE_MSG │ │ ├── ORIG_HEAD │ │ ├── config │ │ ├── description │ │ ├── hooks │ │ │ ├── applypatch-msg.sample │ │ │ ├── commit-msg.sample │ │ │ ├── post-update.sample │ │ │ ├── pre-applypatch.sample │ │ │ ├── pre-commit.sample │ │ │ ├── pre-push.sample │ │ │ ├── pre-rebase.sample │ │ │ ├── prepare-commit-msg.sample │ │ │ └── update.sample │ │ ├── index │ │ ├── info │ │ │ └── exclude │ │ ├── logs │ │ │ ├── HEAD │ │ │ └── refs │ │ │ │ └── heads │ │ │ │ ├── branch │ │ │ │ ├── master │ │ │ │ └── rebaser │ │ └── refs │ │ │ └── heads │ │ │ ├── branch │ │ │ ├── master │ │ │ └── rebaser │ ├── multi-2way-diff.txt │ ├── rebase-2way-diff.txt │ ├── rebasing.git │ │ ├── COMMIT_EDITMSG │ │ ├── HEAD │ │ ├── ORIG_HEAD │ │ ├── config │ │ ├── description │ │ ├── hooks │ │ │ ├── applypatch-msg.sample │ │ │ ├── commit-msg.sample │ │ │ ├── post-update.sample │ │ │ ├── pre-applypatch.sample │ │ │ ├── pre-commit.sample │ │ │ ├── pre-push.sample │ │ │ ├── pre-rebase.sample │ │ │ ├── prepare-commit-msg.sample │ │ │ └── update.sample │ │ ├── index │ │ ├── info │ │ │ └── exclude │ │ ├── logs │ │ │ ├── HEAD │ │ │ └── refs │ │ │ │ └── heads │ │ │ │ ├── branch │ │ │ │ ├── master │ │ │ │ └── rebaser │ │ ├── rebase-apply │ │ │ ├── 0001 │ │ │ ├── 0002 │ │ │ ├── 0003 │ │ │ ├── abort-safety │ │ │ ├── apply-opt │ │ │ ├── author-script │ │ │ ├── final-commit │ │ │ ├── head-name │ │ │ ├── keep │ │ │ ├── last │ │ │ ├── msg-clean │ │ │ ├── next │ │ │ ├── no_inbody_headers │ │ │ ├── onto │ │ │ ├── orig-head │ │ │ ├── original-commit │ │ │ ├── patch │ │ │ ├── quiet │ │ │ ├── rebasing │ │ │ ├── scissors │ │ │ ├── sign │ │ │ ├── threeway │ │ │ └── utf8 │ │ └── refs │ │ │ └── heads │ │ │ ├── branch │ │ │ ├── master │ │ │ └── rebaser │ ├── single-2way-diff.txt │ ├── single-3way-diff-complex.txt │ ├── single-3way-diff.txt │ └── triple-2way-diff.txt ├── git-shellout-spec.coffee ├── util.coffee └── view │ ├── merge-conflicts-view-spec.coffee │ ├── navigation-view-spec.coffee │ ├── resolver-view-spec.coffee │ └── side-view-spec.coffee └── styles ├── merge-conflicts.atom-text-editor.less ├── merge-conflicts.less └── variables.less /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | 3 | notifications: 4 | email: 5 | on_success: never 6 | on_failure: change 7 | 8 | script: 'curl -s https://raw.githubusercontent.com/atom/ci/master/build-package.sh | sh' 9 | 10 | git: 11 | depth: 10 12 | 13 | sudo: false 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.4.4 2 | 3 | - Custom repository contexts and multi-context projects. [#237](https://github.com/smashwilson/merge-conflicts/pull/237) 4 | 5 | ## 1.4.3 6 | 7 | - Normalize paths coming from `git status`. [#236](https://github.com/smashwilson/merge-conflicts/pull/236) 8 | - Avoid overlays rendering over the MergeConflictsView. [#235](https://github.com/smashwilson/merge-conflicts/pull/235) 9 | - Document key binding customization in the README. [#235](https://github.com/smashwilson/merge-conflicts/pull/235) 10 | 11 | ## 1.4.2 12 | 13 | - Rewrite the Conflict parser as a proper recursive descent parser. [#229](https://github.com/smashwilson/merge-conflicts/pull/229) 14 | - Register custom repository contexts to move toward support for non-git repositories. [#222](https://github.com/smashwilson/merge-conflicts/pull/222) 15 | - Transact resolve actions to group as single undo operations. [#221](https://github.com/smashwilson/merge-conflicts/pull/221) 16 | 17 | ## 1.4.1 18 | 19 | - Fix hangs with broken conflict markers. [#220](https://github.com/smashwilson/merge-conflicts/pull/220) 20 | 21 | ## 1.4.0 22 | 23 | - Handle three-way merge markers. [#219](https://github.com/smashwilson/merge-conflicts/pull/219) 24 | - Support for nodegit operations when available, currently in Atom Beta. [#205](https://github.com/smashwilson/merge-conflicts/pull/192) 25 | 26 | ## 1.3.7 27 | 28 | - Resolving entire files as ours or theirs works again. [#192](https://github.com/smashwilson/merge-conflicts/pull/192) 29 | - Use GitUtils to stage files instead of shelling out to git. [#191](https://github.com/smashwilson/merge-conflicts/pull/191) 30 | - Update the method I was using to read the scrollbar width. [#190](https://github.com/smashwilson/merge-conflicts/pull/190) 31 | 32 | ## 1.3.6 33 | 34 | - Use transparency instead of `mix()` to allow selection to show through. [#181](https://github.com/smashwilson/merge-conflicts/pull/181) 35 | - Updated the README to include how-to instructions. [#178](https://github.com/smashwilson/merge-conflicts/pull/178) 36 | 37 | ## 1.3.5 38 | 39 | - Using the "stage" button no longer triggers a crash. [#173](https://github.com/smashwilson/merge-conflicts/pull/173) 40 | 41 | ## 1.3.4 42 | 43 | - Scroll the merge conflicts view when many conflicts exist. [#170](https://github.com/smashwilson/merge-conflicts/pull/170) 44 | - Prune some dead code. [#167](https://github.com/smashwilson/merge-conflicts/pull/167) 45 | 46 | ## 1.3.3 47 | 48 | - With multiple projects, remember the git repository that you initially detected conflicts within. [#165](https://github.com/smashwilson/merge-conflicts/pull/165) 49 | - Handle projects with no git repository. [#164](https://github.com/smashwilson/merge-conflicts/pull/164) 50 | - Improve the "Git not found" error dialog. [#163](https://github.com/smashwilson/merge-conflicts/pull/163) 51 | - Use alt-m instead of ctrl-m in key bindings. [#162](https://github.com/smashwilson/merge-conflicts/pull/162) 52 | - Use Atom's built-in notification API. [#151](https://github.com/smashwilson/merge-conflicts/pull/151) 53 | 54 | ## 1.3.2 55 | 56 | - Use `atom.keymaps` instead of `atom.keymap` for 1.0 compatibility. [#144](https://github.com/smashwilson/merge-conflicts/pull/144) 57 | - Fix a stacktrace when resolving entire files as ours or theirs. [#137](https://github.com/smashwilson/merge-conflicts/pull/137) 58 | 59 | ## 1.3.1 60 | 61 | - Clean up all markers when conflict detection is completed or quit. [#136](https://github.com/smashwilson/merge-conflicts/pull/136) 62 | - Handle next-unresolved or previous-unresolved navigation when conflicts exist in that direction, but all are resolved. [#135](https://github.com/smashwilson/merge-conflicts/pull/135) 63 | 64 | ## 1.3.0 65 | 66 | - Fix more deprecation warnings. [#132](https://github.com/smashwilson/merge-conflicts/pull/132) 67 | 68 | ## 1.2.10 69 | 70 | - Don't decorate destroyed markers. [#124](https://github.com/smashwilson/merge-conflicts/pull/124) 71 | - Missed a fat arrow. [#125](https://github.com/smashwilson/merge-conflicts/pull/125) 72 | - Control subscription cleanup in CoveringViews. [#123](https://github.com/smashwilson/merge-conflicts/pull/123) 73 | 74 | ## 1.2.9 75 | 76 | - It actually works again :wink: 77 | - Use a package-global Emitter instead of `atom.on` [#112](https://github.com/smashwilson/merge-conflicts/pull/112) 78 | - Search additional paths for `git` on Windows [#109](https://github.com/smashwilson/merge-conflicts/pull/109), [#110](https://github.com/smashwilson/merge-conflicts/pull/110) 79 | - Use the `TextEditor` model exclusively, rather than hacking around with a `TextEditorView`. [#108](https://github.com/smashwilson/merge-conflicts/pull/108) 80 | - Require space-pen rather than getting `View` and `$` from Atom itself. [#105](https://github.com/smashwilson/merge-conflicts/pull/105), [#103](https://github.com/smashwilson/merge-conflicts/pull/103) 81 | - Use new-style event subscriptions with an `Emitter` rather than using Emissary mixins. [#104](https://github.com/smashwilson/merge-conflicts/pull/104) 82 | - Update the stylesheets to correctly target elements within the shadow DOM. [#101](https://github.com/smashwilson/merge-conflicts/pull/101) 83 | - Use an overlay decoration rather than injecting controls into the `TextEditorView` DOM directly. [#93](https://github.com/smashwilson/merge-conflicts/pull/93) 84 | 85 | ## 1.2.8 86 | 87 | - Deprecation cop clean sweep! [#89](https://github.com/smashwilson/merge-conflicts/pull/89) 88 | - Search for `git` on your PATH or in common install locations if no specific path is provided. [#88](https://github.com/smashwilson/merge-conflicts/pull/88) 89 | - Correctly reposition `SideViews` when the editor is scrolled. [#87](https://github.com/smashwilson/merge-conflicts/pull/87) 90 | - Render `SideView` controls over the text instead of behind it. [#85](https://github.com/smashwilson/merge-conflicts/pull/87) 91 | 92 | ## 1.2.7 93 | 94 | - Adapt to upstream `EditorView` changes. 95 | 96 | ## 1.2.6 97 | 98 | - Remove deprecated calls to `keyBindingsMatchingElement` and `keystroke`. 99 | 100 | ## 1.2.5 101 | 102 | - Use CSS to distinguish EditorViews instead of `instanceof`. 103 | 104 | ## 1.2.4 105 | 106 | - Use the Decorations API to highlight lines. 107 | 108 | ## 1.2.3 109 | 110 | - Fix a regression in detecting dirty conflict hunks. 111 | - Highlight the cursor line within conflict hunks. 112 | - `Resolve: Ours Then Theirs` and `Resolve: Theirs Then Ours` work properly when rebasing. 113 | - Correct React editor style to accomodate markup changes. 114 | - Use `Ctrl-M` keybindings across all platforms. 115 | - Cosmetic change to the error view. 116 | 117 | ## 1.2.2 118 | 119 | - Work seamlessly across React and Classic editors. 120 | - Show a friendlier error if git isn't found. 121 | 122 | ## 1.2.1 123 | 124 | - Fix resolution context menu items being invoked from a child element. 125 | 126 | ## 1.2.0 127 | 128 | - Consistent keymap entries on Linux, Mac and Windows. 129 | - Detect conflicts with Windows-style line endings. 130 | - Allow the resolver dialog to be dismissed and invoked later. 131 | - Close the resolver dialog on quitting the merge. 132 | - Handle "both added" conflicts. 133 | - Travis CI! 134 | 135 | ## 1.1.0 136 | 137 | - Special handling for conflicts encountered during a rebase. 138 | 139 | ## 1.0.0 140 | 141 | - Identification of conflict markers. 142 | - Superimpose conflict resolution controls. 143 | - Resolve conflicts as either side, directly. 144 | - Resolve conflicts by editing in place. 145 | - Navigation among conflict markers within a file. 146 | - Keymap entries for resolution and navigation. 147 | - Show resolution progress for each file. 148 | - Minify and restore the conflict panel. 149 | - Save and stage changes for each file on completion. 150 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | This package is now **deprecated**: issues and pull requests are ignored. Thank you for your interest just the same :grin: 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Ash Wilson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Merge Conflicts 2 | 3 | ## Deprecation Notice 4 | 5 | This package is **deprecated** and subsumed by the [Git and GitHub integration](https://github.atom.io/) that's bundled with Atom starting with [1.18.0-beta0](https://github.com/atom/atom/releases/tag/v1.18.0-beta0). I highly recommend using that instead! 6 | 7 | [![Build Status](https://travis-ci.org/smashwilson/merge-conflicts.svg?branch=master)](https://travis-ci.org/smashwilson/merge-conflicts) 8 | 9 | Resolve your git merge conflicts in Atom! 10 | 11 | ![conflict-resolution](https://raw.github.com/smashwilson/merge-conflicts/master/docs/conflict-resolution.gif) 12 | 13 | This package detects the conflict markers left by `git merge` and overlays a set of controls for resolving each and navigating among them. Additionally, it displays your progress through a merge. 14 | 15 | ## Installation 16 | ```bash 17 | apm install merge-conflicts 18 | ``` 19 | 20 | ## Features 21 | 22 | * Conflict resolution controls are provided for each detected conflict. 23 | * Choose your version, their version, combinations thereof, or arbitrary changes edited in place as a resolution. 24 | * Navigate to the next and previous conflicts in each file. 25 | * Track your progress through a merge with per-file progress bars and a file list. 26 | * Save and stage your resolved version of each file as it's completed. 27 | 28 | ## Using 29 | 30 | When `git merge` tells you that it couldn't resolve all of your conflicts automatically: 31 | 32 | ``` 33 | $ git merge branch 34 | Auto-merging two 35 | CONFLICT (content): Merge conflict in two 36 | Auto-merging one 37 | CONFLICT (content): Merge conflict in one 38 | Automatic merge failed; fix conflicts and then commit the result. 39 | ``` 40 | 41 | Open Atom on your project and run the command `Merge Conflicts: Detect` (default hotkey: *alt-m d*). You'll see a panel at the bottom of the window describing your progress through the merge: 42 | 43 | ![merge progress](https://raw.github.com/smashwilson/merge-conflicts/master/docs/merge-progress.jpg) 44 | 45 | Click each filename to visit it and step through the identified conflicts. For each conflict area, click "Use me" on either side of the change to accept that side as-is: 46 | 47 | ![conflict area](https://raw.github.com/smashwilson/merge-conflicts/master/docs/conflict-area.jpg) 48 | 49 | Use the right-click menu to choose more advanced resolutions, like "ours then theirs", or edit any chunk by hand then click "use me" to accept your manual modifications. Once you've addressed all of the conflicts within a file, you'll be prompted to save and stage the changes you've made: 50 | 51 | ![save and stage?](https://raw.github.com/smashwilson/merge-conflicts/master/docs/were-done-here.jpg) 52 | 53 | Finally, when *all* of the conflicts throughout the project have been dealt with, a message will appear to prompt you how to commit the resolution and continue on your way. :tada: 54 | 55 | ![onward!](https://raw.github.com/smashwilson/merge-conflicts/master/docs/merge-complete.jpg) 56 | 57 | ## Key bindings 58 | 59 | To customize your key bindings, choose "Keymap..." from your Atom menu and add CSON to bind whatever keys you wish to `merge-conflicts` events. To get started, you can copy and paste this snippet and change the bindings to whatever you prefer: 60 | 61 | ``` 62 | 'atom-text-editor.conflicted': 63 | 'alt-m down': 'merge-conflicts:next-unresolved' 64 | 'alt-m up': 'merge-conflicts:previous-unresolved' 65 | 'alt-m enter': 'merge-conflicts:accept-current' 66 | 'alt-m r': 'merge-conflicts:revert-current' 67 | 'alt-m 1': 'merge-conflicts:accept-ours' 68 | 'alt-m 2': 'merge-conflicts:accept-theirs' 69 | 70 | 'atom-workspace': 71 | 'alt-m d': 'merge-conflicts:detect' 72 | ``` 73 | 74 | For more detail, the Atom docs include both [basic](http://flight-manual.atom.io/using-atom/sections/basic-customization/#_customizing_keybindings) and [advanced](http://flight-manual.atom.io/behind-atom/sections/keymaps-in-depth/) guidelines describing the syntax. 75 | 76 | ## Events 77 | 78 | The merge-conflicts plugin emits a number of events that other packages can subscribe to, if they wish. If you want your plugin to consume one, use code like the following: 79 | 80 | ```coffeescript 81 | {CompositeDisposable} = require 'atom' 82 | 83 | pkg = atom.packages.getActivePackage('merge-conflicts')?.mainModule 84 | subs = new CompositeDisposable 85 | 86 | subs.add pkg.onDidResolveConflict (event) -> 87 | 88 | # ... 89 | 90 | subs.dispose() 91 | ``` 92 | 93 | * `onDidResolveConflict`: broadcast whenever a conflict is resolved. `event.file`: the absolute path of the file in which the conflict was found; `event.total`: the total number of conflicts in that file; `event.resolved`: the number of conflicts that are resolved, including this one. 94 | * `onDidResolveFile`: broadcast whenever a file has been completed and staged for commit. `event.file`: the absolute path of the file that was staged. 95 | * `onDidQuitConflictResolution`: broadcast when you stop merging conflicts by clicking the quit button. 96 | * `onDidCompleteConflictResolution`: broadcast when all conflicts in all files have successfully been resolved. 97 | 98 | ## Contributions 99 | 100 | Pull requests are welcome, big and small! Check out the [contributing guide](./CONTRIBUTING.md) for details. 101 | -------------------------------------------------------------------------------- /docs/announcement.md: -------------------------------------------------------------------------------- 1 | I have bad news and good news to share. 2 | 3 | Bad news first: this package is now officially **deprecated.** It was always a side project that saw activity in bursts every few months as free time, life, and the day job permitted. Now, I will no longer be dedicating any more time to its upkeep, responding to pull requests or issues, or creating new releases. 4 | 5 | The good news is that the reason this is happening is because, beginning with [Atom 1.18.0-beta0](https://atom.io/beta), there's a [much better alternative](https://github.atom.io/) built in to Atom itself! The conflict resolution tools included there should look pretty familiar :wink: 6 | 7 | It won't really hurt anything to have both packages around, but you may want to consider uninstalling: 8 | 9 | ``` 10 | apm uninstall merge-conflicts 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/conflict-area.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smashwilson/merge-conflicts/0ce713e94cb8c08ccccf015d63c2653a589737e2/docs/conflict-area.jpg -------------------------------------------------------------------------------- /docs/conflict-resolution.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smashwilson/merge-conflicts/0ce713e94cb8c08ccccf015d63c2653a589737e2/docs/conflict-resolution.gif -------------------------------------------------------------------------------- /docs/merge-complete.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smashwilson/merge-conflicts/0ce713e94cb8c08ccccf015d63c2653a589737e2/docs/merge-complete.jpg -------------------------------------------------------------------------------- /docs/merge-progress.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smashwilson/merge-conflicts/0ce713e94cb8c08ccccf015d63c2653a589737e2/docs/merge-progress.jpg -------------------------------------------------------------------------------- /docs/useful-test-cases.md: -------------------------------------------------------------------------------- 1 | # Ansible core 2 | 3 | git checkout 7806c50 4 | git merge 4e0c01b 5 | 6 | git checkout 3a015a7 7 | git merge 6676720 8 | -------------------------------------------------------------------------------- /docs/were-done-here.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smashwilson/merge-conflicts/0ce713e94cb8c08ccccf015d63c2653a589737e2/docs/were-done-here.jpg -------------------------------------------------------------------------------- /keymaps/merge-conflicts.cson: -------------------------------------------------------------------------------- 1 | # Keybindings require three things to be fully defined: A selector that is 2 | # matched against the focused element, the keystroke and the command to 3 | # execute. 4 | # 5 | # Below is a basic keybinding which registers on all platforms by applying to 6 | # the root workspace element. 7 | 8 | # For more detailed documentation see 9 | # https://atom.io/docs/latest/advanced/keymaps 10 | 11 | 'atom-text-editor.conflicted': 12 | 'alt-m down': 'merge-conflicts:next-unresolved' 13 | 'alt-m up': 'merge-conflicts:previous-unresolved' 14 | 'alt-m enter': 'merge-conflicts:accept-current' 15 | 'alt-m r': 'merge-conflicts:revert-current' 16 | 'alt-m 1': 'merge-conflicts:accept-ours' 17 | 'alt-m 2': 'merge-conflicts:accept-theirs' 18 | 19 | 'atom-workspace': 20 | 'alt-m d': 'merge-conflicts:detect' 21 | -------------------------------------------------------------------------------- /lib/atom_merge: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | MERGED=${MERGED:-$1} 4 | if [ -z "${MERGED}" ] ; then 5 | echo ERR 6 | echo Need to have a file to merge 7 | exit 1 8 | fi 9 | 10 | #Wait for changes on the merge file 11 | echo Watching for changes in ${MERGED} 12 | bash -c "while [ $(stat -f %c $MERGED) -eq \$(stat -f %c $MERGED) ] ; do sleep 1 ; done " & 13 | PID_CHANGED=$! 14 | 15 | #Spawn atom, pointed at the file to merge. Up to the user to edit 16 | echo Opening ${MERGED} in atom 17 | atom $MERGED & 18 | 19 | #Once the file has been saved, we assume merge is complete. 20 | #User will need to acknowledge in shell 21 | wait $PID_CHANGED 22 | -------------------------------------------------------------------------------- /lib/conflict.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | import {Emitter} from 'atom' 4 | import _ from 'underscore-plus' 5 | 6 | import {Side, OurSide, TheirSide, BaseSide} from './side' 7 | import {Navigator} from './navigator' 8 | 9 | // Public: Model an individual conflict parsed from git's automatic conflict resolution output. 10 | export class Conflict { 11 | 12 | /* 13 | * Private: Initialize a new Conflict with its constituent Sides, Navigator, and the MergeState 14 | * it belongs to. 15 | * 16 | * ours [Side] the lines of this conflict that the current user contributed (by our best guess). 17 | * theirs [Side] the lines of this conflict that another contributor created. 18 | * base [Side] the lines of merge base of this conflict. Optional. 19 | * navigator [Navigator] maintains references to surrounding Conflicts in the original file. 20 | * state [MergeState] repository-wide information about the current merge. 21 | */ 22 | constructor (ours, theirs, base, navigator, merge) { 23 | this.ours = ours 24 | this.theirs = theirs 25 | this.base = base 26 | this.navigator = navigator 27 | this.merge = merge 28 | 29 | this.emitter = new Emitter() 30 | 31 | // Populate back-references 32 | this.ours.conflict = this 33 | this.theirs.conflict = this 34 | if (this.base) { 35 | this.base.conflict = this 36 | } 37 | this.navigator.conflict = this 38 | 39 | // Begin unresolved 40 | this.resolution = null 41 | } 42 | 43 | /* 44 | * Public: Has this conflict been resolved in any way? 45 | * 46 | * Return [Boolean] 47 | */ 48 | isResolved() { 49 | return this.resolution !== null 50 | } 51 | 52 | /* 53 | * Public: Attach an event handler to be notified when this conflict is resolved. 54 | * 55 | * callback [Function] 56 | */ 57 | onDidResolveConflict (callback) { 58 | return this.emitter.on('resolve-conflict', callback) 59 | } 60 | 61 | /* 62 | * Public: Specify which Side is to be kept. Note that either side may have been modified by the 63 | * user prior to resolution. Notify any subscribers. 64 | * 65 | * side [Side] our changes or their changes. 66 | */ 67 | resolveAs (side) { 68 | this.resolution = side 69 | this.emitter.emit('resolve-conflict') 70 | } 71 | 72 | /* 73 | * Public: Locate the position that the editor should scroll to in order to make this conflict 74 | * visible. 75 | * 76 | * Return [Point] buffer coordinates 77 | */ 78 | scrollTarget () { 79 | return this.ours.marker.getTailBufferPosition() 80 | } 81 | 82 | /* 83 | * Public: Audit all Marker instances owned by subobjects within this Conflict. 84 | * 85 | * Return [Array] 86 | */ 87 | markers () { 88 | const ms = [this.ours.markers(), this.theirs.markers(), this.navigator.markers()] 89 | if (this.base) { 90 | ms.push(this.base.markers()) 91 | } 92 | return _.flatten(ms, true) 93 | } 94 | 95 | /* 96 | * Public: Console-friendly identification of this conflict. 97 | * 98 | * Return [String] that distinguishes this conflict from others. 99 | */ 100 | toString () { 101 | return `[conflict: ${this.ours} ${this.theirs}]` 102 | } 103 | 104 | /* 105 | * Public: Parse any conflict markers in a TextEditor's buffer and return a Conflict that contains 106 | * markers corresponding to each. 107 | * 108 | * merge [MergeState] Repository-wide state of the merge. 109 | * editor [TextEditor] The editor to search. 110 | * return [Array] A (possibly empty) collection of parsed Conflicts. 111 | */ 112 | static all (merge, editor) { 113 | const conflicts = [] 114 | let lastRow = -1 115 | 116 | editor.getBuffer().scan(CONFLICT_START_REGEX, (m) => { 117 | conflictStartRow = m.range.start.row 118 | if (conflictStartRow < lastRow) { 119 | // Match within an already-parsed conflict. 120 | return 121 | } 122 | 123 | const visitor = new ConflictVisitor(merge, editor) 124 | 125 | try { 126 | lastRow = parseConflict(merge, editor, conflictStartRow, visitor) 127 | const conflict = visitor.conflict() 128 | 129 | if (conflicts.length > 0) { 130 | conflict.navigator.linkToPrevious(conflicts[conflicts.length - 1]) 131 | } 132 | conflicts.push(conflict) 133 | } catch (e) { 134 | if (!e.parserState) throw e 135 | 136 | if (!atom.inSpecMode()) { 137 | console.error(`Unable to parse conflict: ${e.message}\n${e.stack}`) 138 | } 139 | } 140 | }) 141 | 142 | return conflicts 143 | } 144 | } 145 | 146 | // Regular expression that matches the beginning of a potential conflict. 147 | const CONFLICT_START_REGEX = /^<{7} (.+)\r?\n/g 148 | 149 | // Side positions. 150 | const TOP = 'top' 151 | const BASE = 'base' 152 | const BOTTOM = 'bottom' 153 | 154 | // Options used to initialize markers. 155 | const options = { 156 | invalidate: 'never' 157 | } 158 | 159 | /* 160 | * Private: conflict parser visitor that ignores all events. 161 | */ 162 | class NoopVisitor { 163 | 164 | visitOurSide (position, bannerRow, textRowStart, textRowEnd) { } 165 | 166 | visitBaseSide (position, bannerRow, textRowStart, textRowEnd) { } 167 | 168 | visitSeparator (sepRowStart, sepRowEnd) { } 169 | 170 | visitTheirSide (position, bannerRow, textRowStart, textRowEnd) { } 171 | 172 | } 173 | 174 | /* 175 | * Private: conflict parser visitor that marks each buffer range and assembles a Conflict from the 176 | * pieces. 177 | */ 178 | class ConflictVisitor { 179 | 180 | /* 181 | * merge - [MergeState] passed to each instantiated Side. 182 | * editor - [TextEditor] displaying the conflicting text. 183 | */ 184 | constructor (merge, editor) { 185 | this.merge = merge 186 | this.editor = editor 187 | this.previousSide = null 188 | 189 | this.ourSide = null 190 | this.baseSide = null 191 | this.navigator = null 192 | } 193 | 194 | /* 195 | * position - [String] one of TOP or BOTTOM. 196 | * bannerRow - [Integer] of the buffer row that contains our side's banner. 197 | * textRowStart - [Integer] of the first buffer row that contain this side's text. 198 | * textRowEnd - [Integer] of the first buffer row beyond the extend of this side's text. 199 | */ 200 | visitOurSide (position, bannerRow, textRowStart, textRowEnd) { 201 | this.ourSide = this.markSide(position, OurSide, bannerRow, textRowStart, textRowEnd) 202 | } 203 | 204 | /* 205 | * bannerRow - [Integer] the buffer row that contains our side's banner. 206 | * textRowStart - [Integer] first buffer row that contain this side's text. 207 | * textRowEnd - [Integer] first buffer row beyond the extend of this side's text. 208 | */ 209 | visitBaseSide (bannerRow, textRowStart, textRowEnd) { 210 | this.baseSide = this.markSide(BASE, BaseSide, bannerRow, textRowStart, textRowEnd) 211 | } 212 | 213 | /* 214 | * sepRowStart - [Integer] buffer row that contains the "=======" separator. 215 | * sepRowEnd - [Integer] the buffer row after the separator. 216 | */ 217 | visitSeparator (sepRowStart, sepRowEnd) { 218 | const marker = this.editor.markBufferRange([[sepRowStart, 0], [sepRowEnd, 0]], options) 219 | this.previousSide.followingMarker = marker 220 | 221 | this.navigator = new Navigator(marker) 222 | this.previousSide = this.navigator 223 | } 224 | 225 | /* 226 | * position - [String] Always BASE; accepted for consistency. 227 | * bannerRow - [Integer] the buffer row that contains our side's banner. 228 | * textRowStart - [Integer] first buffer row that contain this side's text. 229 | * textRowEnd - [Integer] first buffer row beyond the extend of this side's text. 230 | */ 231 | visitTheirSide (position, bannerRow, textRowStart, textRowEnd) { 232 | this.theirSide = this.markSide(position, TheirSide, bannerRow, textRowStart, textRowEnd) 233 | } 234 | 235 | markSide (position, sideKlass, bannerRow, textRowStart, textRowEnd) { 236 | const description = this.sideDescription(bannerRow) 237 | 238 | const bannerMarker = this.editor.markBufferRange([[bannerRow, 0], [bannerRow + 1, 0]], options) 239 | 240 | if (this.previousSide) { 241 | this.previousSide.followingMarker = bannerMarker 242 | } 243 | 244 | const textRange = [[textRowStart, 0], [textRowEnd, 0]] 245 | const textMarker = this.editor.markBufferRange(textRange, options) 246 | const text = this.editor.getTextInBufferRange(textRange) 247 | 248 | const side = new sideKlass(text, description, textMarker, bannerMarker, position) 249 | this.previousSide = side 250 | return side 251 | } 252 | 253 | /* 254 | * Parse the banner description for the current side from a banner row. 255 | */ 256 | sideDescription (bannerRow) { 257 | return this.editor.lineTextForBufferRow(bannerRow).match(/^[<|>]{7} (.*)$/)[1] 258 | } 259 | 260 | conflict () { 261 | this.previousSide.followingMarker = this.previousSide.refBannerMarker 262 | 263 | return new Conflict(this.ourSide, this.theirSide, this.baseSide, this.navigator, this.merge) 264 | } 265 | 266 | } 267 | 268 | /* 269 | * Private: parseConflict discovers git conflict markers in a corpus of text and constructs Conflict 270 | * instances that mark the correct lines. 271 | * 272 | * Returns [Integer] the buffer row after the final <<<<<< boundary. 273 | */ 274 | const parseConflict = function (merge, editor, row, visitor) { 275 | let lastBoundary = null 276 | 277 | // Visit a side that begins with a banner and description as its first line. 278 | const visitHeaderSide = (position, visitMethod) => { 279 | const sideRowStart = row 280 | row += 1 281 | advanceToBoundary('|=') 282 | const sideRowEnd = row 283 | 284 | visitor[visitMethod](position, sideRowStart, sideRowStart + 1, sideRowEnd) 285 | } 286 | 287 | // Visit the base side from diff3 output, if one is present, then visit the separator. 288 | const visitBaseAndSeparator = () => { 289 | if (lastBoundary === '|') { 290 | visitBaseSide() 291 | } 292 | 293 | visitSeparator() 294 | } 295 | 296 | // Visit a base side from diff3 output. 297 | const visitBaseSide = () => { 298 | const sideRowStart = row 299 | row += 1 300 | 301 | let b = advanceToBoundary('<=') 302 | while (b === '<') { 303 | // Embedded recursive conflict within a base side, caused by a criss-cross merge. 304 | // Advance beyond it without marking anything. 305 | row = parseConflict(merge, editor, row, new NoopVisitor()) 306 | b = advanceToBoundary('<=') 307 | } 308 | 309 | const sideRowEnd = row 310 | 311 | visitor.visitBaseSide(sideRowStart, sideRowStart + 1, sideRowEnd) 312 | } 313 | 314 | // Visit a "========" separator. 315 | const visitSeparator = () => { 316 | const sepRowStart = row 317 | row += 1 318 | const sepRowEnd = row 319 | 320 | visitor.visitSeparator(sepRowStart, sepRowEnd) 321 | } 322 | 323 | // Vidie a side with a banner and description as its last line. 324 | const visitFooterSide = (position, visitMethod) => { 325 | const sideRowStart = row 326 | const b = advanceToBoundary('>') 327 | row += 1 328 | sideRowEnd = row 329 | 330 | visitor[visitMethod](position, sideRowEnd - 1, sideRowStart, sideRowEnd - 1) 331 | } 332 | 333 | // Determine if the current row is a side boundary. 334 | // 335 | // boundaryKinds - [String] any combination of <, |, =, or > to limit the kinds of boundary 336 | // detected. 337 | // 338 | // Returns the matching boundaryKinds character, or `null` if none match. 339 | const isAtBoundary = (boundaryKinds = '<|=>') => { 340 | const line = editor.lineTextForBufferRow(row) 341 | for (b of boundaryKinds) { 342 | if (line.startsWith(b.repeat(7))) { 343 | return b 344 | } 345 | } 346 | return null 347 | } 348 | 349 | // Increment the current row until the current line matches one of the provided boundary kinds, 350 | // or until there are no more lines in the editor. 351 | // 352 | // boundaryKinds - [String] any combination of <, |, =, or > to limit the kinds of boundaries 353 | // that halt the progression. 354 | // 355 | // Returns the matching boundaryKinds character, or 'null' if there are no matches to the end of 356 | // the editor. 357 | const advanceToBoundary = (boundaryKinds = '<|=>') => { 358 | let b = isAtBoundary(boundaryKinds) 359 | while (b === null) { 360 | row += 1 361 | if (row > editor.getLastBufferRow()) { 362 | const e = new Error('Unterminated conflict side') 363 | e.parserState = true 364 | throw e 365 | } 366 | b = isAtBoundary(boundaryKinds) 367 | } 368 | 369 | lastBoundary = b 370 | return b 371 | } 372 | 373 | if (!merge.isRebase) { 374 | visitHeaderSide(TOP, 'visitOurSide') 375 | visitBaseAndSeparator() 376 | visitFooterSide(BOTTOM, 'visitTheirSide') 377 | } else { 378 | visitHeaderSide(TOP, 'visitTheirSide') 379 | visitBaseAndSeparator() 380 | visitFooterSide(BOTTOM, 'visitOurSide') 381 | } 382 | 383 | return row 384 | } 385 | -------------------------------------------------------------------------------- /lib/conflicted-editor.coffee: -------------------------------------------------------------------------------- 1 | {$} = require 'space-pen' 2 | _ = require 'underscore-plus' 3 | {Emitter, CompositeDisposable} = require 'atom' 4 | 5 | {Conflict} = require './conflict' 6 | 7 | {SideView} = require './view/side-view' 8 | {NavigationView} = require './view/navigation-view' 9 | {ResolverView} = require './view/resolver-view' 10 | 11 | # Public: Mediate conflict-related decorations and events on behalf of a specific TextEditor. 12 | # 13 | class ConflictedEditor 14 | 15 | # Public: Instantiate a new ConflictedEditor to manage the decorations and events of a specific 16 | # TextEditor. 17 | # 18 | # state [MergeState] - Merge-wide conflict state. 19 | # pkg [Emitter] - The package object containing event dispatch and subscription methods. 20 | # editor [TextEditor] - An editor containing text that, presumably, includes conflict markers. 21 | # 22 | constructor: (@state, @pkg, @editor) -> 23 | @subs = new CompositeDisposable 24 | @coveringViews = [] 25 | @conflicts = [] 26 | 27 | # Public: Locate Conflicts within this specific TextEditor. 28 | # 29 | # Install a pair of SideViews and a NavigationView for each Conflict discovered within the 30 | # editor's text. Subscribe to package events related to relevant Conflicts and broadcast 31 | # per-editor progress events as they are resolved. Install Atom commands related to conflict 32 | # navigation and resolution. 33 | # 34 | mark: -> 35 | @conflicts = Conflict.all(@state, @editor) 36 | 37 | @coveringViews = [] 38 | for c in @conflicts 39 | @coveringViews.push new SideView(c.ours, @editor) 40 | @coveringViews.push new SideView(c.base, @editor) if c.base? 41 | @coveringViews.push new NavigationView(c.navigator, @editor) 42 | @coveringViews.push new SideView(c.theirs, @editor) 43 | 44 | @subs.add c.onDidResolveConflict => 45 | unresolved = (v for v in @coveringViews when not v.conflict().isResolved()) 46 | resolvedCount = @conflicts.length - Math.floor(unresolved.length / 3) 47 | @pkg.didResolveConflict 48 | file: @editor.getPath(), 49 | total: @conflicts.length, resolved: resolvedCount, 50 | source: this 51 | 52 | if @conflicts.length > 0 53 | atom.views.getView(@editor).classList.add 'conflicted' 54 | 55 | cv.decorate() for cv in @coveringViews 56 | @installEvents() 57 | @focusConflict @conflicts[0] 58 | else 59 | @pkg.didResolveConflict 60 | file: @editor.getPath(), 61 | total: 1, resolved: 1, 62 | source: this 63 | @conflictsResolved() 64 | 65 | # Private: Install Atom commands related to Conflict resolution and navigation on the TextEditor. 66 | # 67 | # Listen for package-global events that relate to the local Conflicts and dispatch them 68 | # appropriately. 69 | # 70 | installEvents: -> 71 | @subs.add @editor.onDidStopChanging => @detectDirty() 72 | @subs.add @editor.onDidDestroy => @cleanup() 73 | 74 | @subs.add atom.commands.add 'atom-text-editor', 75 | 'merge-conflicts:accept-current': => @acceptCurrent(), 76 | 'merge-conflicts:accept-ours': => @acceptOurs(), 77 | 'merge-conflicts:accept-theirs': => @acceptTheirs(), 78 | 'merge-conflicts:ours-then-theirs': => @acceptOursThenTheirs(), 79 | 'merge-conflicts:theirs-then-ours': => @acceptTheirsThenOurs(), 80 | 'merge-conflicts:next-unresolved': => @nextUnresolved(), 81 | 'merge-conflicts:previous-unresolved': => @previousUnresolved(), 82 | 'merge-conflicts:revert-current': => @revertCurrent() 83 | 84 | @subs.add @pkg.onDidResolveConflict ({total, resolved, file}) => 85 | if file is @editor.getPath() and total is resolved 86 | @conflictsResolved() 87 | 88 | @subs.add @pkg.onDidCompleteConflictResolution => @cleanup() 89 | @subs.add @pkg.onDidQuitConflictResolution => @cleanup() 90 | 91 | # Private: Undo any changes done to the underlying TextEditor. 92 | # 93 | cleanup: -> 94 | atom.views.getView(@editor).classList.remove 'conflicted' if @editor? 95 | 96 | for c in @conflicts 97 | m.destroy() for m in c.markers() 98 | 99 | v.remove() for v in @coveringViews 100 | 101 | @subs.dispose() 102 | 103 | # Private: Event handler invoked when all conflicts in this file have been resolved. 104 | # 105 | conflictsResolved: -> 106 | atom.workspace.addTopPanel item: new ResolverView(@editor, @state, @pkg) 107 | 108 | detectDirty: -> 109 | # Only detect dirty regions within CoveringViews that have a cursor within them. 110 | potentials = [] 111 | for c in @editor.getCursors() 112 | for v in @coveringViews 113 | potentials.push(v) if v.includesCursor(c) 114 | 115 | v.detectDirty() for v in _.uniq(potentials) 116 | 117 | # Private: Command that accepts each side of a conflict that contains a cursor. 118 | # 119 | # Conflicts with cursors in both sides will be ignored. 120 | # 121 | acceptCurrent: -> 122 | return unless @editor is atom.workspace.getActiveTextEditor() 123 | 124 | sides = @active() 125 | 126 | # Do nothing if you have cursors in *both* sides of a single conflict. 127 | duplicates = [] 128 | seen = {} 129 | for side in sides 130 | if side.conflict of seen 131 | duplicates.push side 132 | duplicates.push seen[side.conflict] 133 | seen[side.conflict] = side 134 | sides = _.difference sides, duplicates 135 | 136 | @editor.transact -> 137 | side.resolve() for side in sides 138 | 139 | # Private: Command that accepts the "ours" side of the active conflict. 140 | # 141 | acceptOurs: -> 142 | return unless @editor is atom.workspace.getActiveTextEditor() 143 | @editor.transact => 144 | side.conflict.ours.resolve() for side in @active() 145 | 146 | # Private: Command that accepts the "theirs" side of the active conflict. 147 | # 148 | acceptTheirs: -> 149 | return unless @editor is atom.workspace.getActiveTextEditor() 150 | @editor.transact => 151 | side.conflict.theirs.resolve() for side in @active() 152 | 153 | # Private: Command that uses a composite resolution of the "ours" side followed by the "theirs" 154 | # side of the active conflict. 155 | # 156 | acceptOursThenTheirs: -> 157 | return unless @editor is atom.workspace.getActiveTextEditor() 158 | @editor.transact => 159 | for side in @active() 160 | @combineSides side.conflict.ours, side.conflict.theirs 161 | 162 | # Private: Command that uses a composite resolution of the "theirs" side followed by the "ours" 163 | # side of the active conflict. 164 | # 165 | acceptTheirsThenOurs: -> 166 | return unless @editor is atom.workspace.getActiveTextEditor() 167 | @editor.transact => 168 | for side in @active() 169 | @combineSides side.conflict.theirs, side.conflict.ours 170 | 171 | # Private: Command that navigates to the next unresolved conflict in the editor. 172 | # 173 | # If the cursor is on or after the final unresolved conflict in the editor, nothing happens. 174 | # 175 | nextUnresolved: -> 176 | return unless @editor is atom.workspace.getActiveTextEditor() 177 | final = _.last @active() 178 | if final? 179 | n = final.conflict.navigator.nextUnresolved() 180 | @focusConflict(n) if n? 181 | else 182 | orderedCursors = _.sortBy @editor.getCursors(), (c) -> 183 | c.getBufferPosition().row 184 | lastCursor = _.last orderedCursors 185 | return unless lastCursor? 186 | 187 | pos = lastCursor.getBufferPosition() 188 | firstAfter = null 189 | for c in @conflicts 190 | p = c.ours.marker.getBufferRange().start 191 | if p.isGreaterThanOrEqual(pos) and not firstAfter? 192 | firstAfter = c 193 | return unless firstAfter? 194 | 195 | if firstAfter.isResolved() 196 | target = firstAfter.navigator.nextUnresolved() 197 | else 198 | target = firstAfter 199 | return unless target? 200 | 201 | @focusConflict target 202 | 203 | # Private: Command that navigates to the previous unresolved conflict in the editor. 204 | # 205 | # If the cursor is on or before the first unresolved conflict in the editor, nothing happens. 206 | # 207 | previousUnresolved: -> 208 | return unless @editor is atom.workspace.getActiveTextEditor() 209 | initial = _.first @active() 210 | if initial? 211 | p = initial.conflict.navigator.previousUnresolved() 212 | @focusConflict(p) if p? 213 | else 214 | orderedCursors = _.sortBy @editor.getCursors(), (c) -> 215 | c.getBufferPosition().row 216 | firstCursor = _.first orderedCursors 217 | return unless firstCursor? 218 | 219 | pos = firstCursor.getBufferPosition() 220 | lastBefore = null 221 | for c in @conflicts 222 | p = c.ours.marker.getBufferRange().start 223 | if p.isLessThanOrEqual pos 224 | lastBefore = c 225 | return unless lastBefore? 226 | 227 | if lastBefore.isResolved() 228 | target = lastBefore.navigator.previousUnresolved() 229 | else 230 | target = lastBefore 231 | return unless target? 232 | 233 | @focusConflict target 234 | 235 | # Private: Revert manual edits to the current side of the active conflict. 236 | # 237 | revertCurrent: -> 238 | return unless @editor is atom.workspace.getActiveTextEditor() 239 | for side in @active() 240 | for view in @coveringViews when view.conflict() is side.conflict 241 | view.revert() if view.isDirty() 242 | 243 | # Private: Collect a list of each Side of any Conflict within the editor that contains a cursor. 244 | # 245 | # Returns [Array] 246 | # 247 | active: -> 248 | positions = (c.getBufferPosition() for c in @editor.getCursors()) 249 | matching = [] 250 | for c in @conflicts 251 | for p in positions 252 | if c.ours.marker.getBufferRange().containsPoint p 253 | matching.push c.ours 254 | if c.theirs.marker.getBufferRange().containsPoint p 255 | matching.push c.theirs 256 | matching 257 | 258 | # Private: Resolve a conflict by combining its two Sides in a specific order. 259 | # 260 | # first [Side] The Side that should occur first in the resolved text. 261 | # second [Side] The Side belonging to the same Conflict that should occur second in the resolved 262 | # text. 263 | # 264 | combineSides: (first, second) -> 265 | text = @editor.getTextInBufferRange second.marker.getBufferRange() 266 | e = first.marker.getBufferRange().end 267 | insertPoint = @editor.setTextInBufferRange([e, e], text).end 268 | first.marker.setHeadBufferPosition insertPoint 269 | first.followingMarker.setTailBufferPosition insertPoint 270 | first.resolve() 271 | 272 | # Private: Scroll the editor and place the cursor at the beginning of a marked conflict. 273 | # 274 | # conflict [Conflict] Any conflict within the current editor. 275 | # 276 | focusConflict: (conflict) -> 277 | st = conflict.ours.marker.getBufferRange().start 278 | @editor.scrollToBufferPosition st, center: true 279 | @editor.setCursorBufferPosition st, autoscroll: false 280 | 281 | module.exports = 282 | ConflictedEditor: ConflictedEditor 283 | -------------------------------------------------------------------------------- /lib/git/common.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // Utilities shared among git backends. 4 | 5 | exports.getActiveGitRepo = function (filePath) { 6 | if (!filePath) { 7 | filePath = getActivePath(); 8 | } 9 | let dirs = atom.project.getDirectories(); 10 | let repos = atom.project.getRepositories(); 11 | for (let i = 0; i < dirs.length; i++) { 12 | let d = dirs[i]; 13 | if (d.contains(filePath) && isGitRepo(repos[i])) { 14 | return Promise.resolve({ 15 | repository: repos[i], 16 | priority: 3, 17 | }); 18 | } 19 | } 20 | 21 | const firstGitRepo = repos.filter(isGitRepo)[0]; 22 | return Promise.resolve(firstGitRepo == null ? null : { 23 | repository: firstGitRepo, 24 | priority: 2, 25 | }); 26 | }; 27 | 28 | function isGitRepo(repo) { 29 | return repo != null && repo.getType() === 'git'; 30 | } 31 | 32 | function getActivePath() { 33 | let paneItem = atom.workspace.getActivePaneItem(); 34 | 35 | if (!paneItem) return null; 36 | if (!paneItem.getPath) return null; 37 | 38 | return paneItem.getPath(); 39 | } 40 | 41 | exports.quitContext = function (wasRebasing) { 42 | let detail = "Careful, you've still got conflict markers left!\n"; 43 | if (wasRebasing) 44 | detail += '"git rebase --abort"'; 45 | else 46 | detail += '"git merge --abort"'; 47 | detail += " if you just want to give up on this one."; 48 | atom.notifications.addWarning("Maybe Later", { 49 | detail: detail, 50 | dismissable: true, 51 | }); 52 | } 53 | 54 | exports.completeContext = function (wasRebasing) { 55 | let detail = "That's everything. "; 56 | if (wasRebasing) 57 | detail += '"git rebase --continue" at will to resume rebasing.'; 58 | else 59 | detail += '"git commit" at will to finish the merge.'; 60 | 61 | atom.notifications.addSuccess("All Conflicts Resolved", { 62 | detail: detail, 63 | dismissable: true, 64 | }); 65 | } 66 | -------------------------------------------------------------------------------- /lib/git/gitops.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // Git operations backed by nodegit. 4 | 5 | let a = require("atom"); 6 | let Git = a.GitRepositoryAsync.Git; 7 | let Checkout = Git.Checkout; 8 | let Directory = a.Directory; 9 | let common = require("./common"); 10 | 11 | exports.getContext = function (filePath) { 12 | let wd = null; 13 | 14 | return common.getActiveGitRepo(filePath) 15 | .then((repoDetails) => { 16 | if (!repoDetails) return null; 17 | 18 | const {repository, priority} = repoDetails; 19 | 20 | wd = repository.getWorkingDirectory(); 21 | 22 | return Git.Repository.open(wd); 23 | }) 24 | .then((nodegitRepo) => { 25 | if (!nodegitRepo) return null; 26 | 27 | return new GitContext(nodegitRepo, wd, priority); 28 | }); 29 | }; 30 | 31 | function GitContext(repository, workingDirPath, priority) { 32 | this.repository = repository; 33 | this.workingDirPath = workingDirPath; 34 | this.workingDirectory = new Directory(workingDirPath, false); 35 | this.priority = priority; 36 | this.resolveText = "Stage"; 37 | } 38 | 39 | GitContext.prototype.readConflicts = function () { 40 | return this.repository.getStatus() 41 | .then((statuses) => { 42 | let conflicts = []; 43 | 44 | statuses.forEach((status) => { 45 | if (status.isConflicted()) { 46 | conflicts.push({ 47 | path: status.path(), 48 | message: "both modified" 49 | }); 50 | } 51 | 52 | // TODO: detect "both added" 53 | }); 54 | 55 | return conflicts; 56 | }) 57 | }; 58 | 59 | GitContext.prototype.isResolvedFile = function (filePath) { 60 | return this.repository.getStatus() 61 | .then((statuses) => statuses.some((status) => { 62 | return status.path() === filePath && status.isModified(); 63 | })); 64 | }; 65 | 66 | GitContext.prototype.checkoutSide = function (sideName, filePath) { 67 | let strategy = 0; 68 | switch (sideName) { 69 | case "ours": 70 | strategy = Checkout.STRATEGY.USE_OURS; 71 | break; 72 | case "theirs": 73 | strategy = Checkout.STRATEGY.USE_THEIRS; 74 | break; 75 | default: 76 | return new Promise.reject(new Error(`Unrecognized sideName: [${sideName}]`)); 77 | } 78 | 79 | return Checkout.head(this.repository, { 80 | checkoutStrategy: strategy 81 | }); 82 | }; 83 | 84 | GitContext.prototype.resolveFile = function (filePath) { 85 | return this.repository.index() 86 | .then((i) => { 87 | let result = i.addByPath(filePath); 88 | if (result === 0) { 89 | return Promise.resolve(); 90 | } else { 91 | return Promise.reject(new Error(`Index#addByPath error code: [${result}]`)); 92 | } 93 | }) 94 | }; 95 | 96 | GitContext.prototype.isRebasing = function () { 97 | return this.repository.isRebasing(); 98 | }; 99 | 100 | GitContext.prototype.joinPath = function(relativePath) { 101 | return path.join(this.workingDirPath, relativePath); 102 | } 103 | 104 | GitContext.prototype.quit = function(wasRebasing) { 105 | common.quitContext(wasRebasing); 106 | } 107 | 108 | GitContext.prototype.complete = function(wasRebasing) { 109 | common.completeContext(wasRebasing); 110 | } 111 | -------------------------------------------------------------------------------- /lib/git/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | let a = require("atom"); 4 | 5 | // Feature-flagged out 6 | if (process.env.USE_NODEGIT === 'yes' && a.GitRepositoryAsync) { 7 | exports.GitOps = require("./gitops"); 8 | } else { 9 | // nodegit is not yet available. Fall back to the shell-out version. 10 | exports.GitOps = require("./shellout"); 11 | } 12 | -------------------------------------------------------------------------------- /lib/git/shellout.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // Git operations backed by shelling out. 4 | 5 | let a = require("atom"); 6 | let BufferedProcess = a.BufferedProcess; 7 | let Directory = a.Directory; 8 | let fs = require("fs"); 9 | let path = require("path"); 10 | 11 | let common = require("./common"); 12 | 13 | exports.getContext = function (filePath) { 14 | return Promise.all([locateGit(), common.getActiveGitRepo(filePath)]) 15 | .then((results) => { 16 | const gitCmd = results[0]; 17 | const repoDetails = results[1]; 18 | 19 | if (!gitCmd || !repoDetails) return null; 20 | 21 | const repository = repoDetails.repository; 22 | const priority = repoDetails.priority; 23 | let wd = repository.getWorkingDirectory(); 24 | return new GitContext(repository, gitCmd, wd, priority); 25 | }); 26 | }; 27 | 28 | function GitContext(repository, gitCmd, workingDirPath, priority) { 29 | this.repository = repository; 30 | this.gitCmd = gitCmd; 31 | this.workingDirPath = workingDirPath; 32 | this.workingDirectory = new Directory(workingDirPath, false); 33 | this.runProcess = (args) => new BufferedProcess(args); 34 | this.priority = priority; 35 | this.resolveText = "Stage"; 36 | }; 37 | 38 | GitContext.prototype.readConflicts = function () { 39 | let conflicts = []; 40 | let errMessage = []; 41 | 42 | return new Promise((resolve, reject) => { 43 | let stdoutHandler = (chunk) => { 44 | statusCodesFrom(chunk, (index, work, p) => { 45 | if (index === "U" && work === "U") { 46 | conflicts.push({ 47 | path: path.normalize(p), 48 | message: "both modified", 49 | }); 50 | } 51 | 52 | if (index === "A" && work === "A") { 53 | conflicts.push({ 54 | path: path.normalize(p), 55 | message: "both added", 56 | }); 57 | } 58 | }); 59 | }; 60 | 61 | let stderrHandler = (line) => errMessage.push(line); 62 | 63 | let exitHandler = (code) => { 64 | if (code === 0) { 65 | return resolve(conflicts); 66 | } 67 | 68 | return reject(new Error(`abnormal git exit: ${code}\n${errMessage.join("\n")}`)); 69 | }; 70 | 71 | let proc = this.runProcess({ 72 | command: this.gitCmd, 73 | args: ['status', '--porcelain'], 74 | options: { cwd: this.workingDirPath }, 75 | stdout: stdoutHandler, 76 | stderr: stderrHandler, 77 | exit: exitHandler 78 | }); 79 | 80 | proc.process.on("error", reject); 81 | }); 82 | }; 83 | 84 | GitContext.prototype.isResolvedFile = function (filePath) { 85 | let staged = true; 86 | 87 | return new Promise((resolve, reject) => { 88 | let stdoutHandler = (chunk) => { 89 | statusCodesFrom(chunk, (index, work, p) => { 90 | if (path.normalize(p) === filePath) { 91 | staged = index === "M" && work === " "; 92 | } 93 | }); 94 | }; 95 | 96 | let stderrHandler = console.error; 97 | 98 | let exitHandler = (code) => { 99 | if (code === 0) { 100 | resolve(staged); 101 | } else { 102 | reject(new Error(`git status exit: ${code}`)); 103 | } 104 | }; 105 | 106 | let proc = this.runProcess({ 107 | command: this.gitCmd, 108 | args: ["status", "--porcelain", filePath], 109 | options: { cwd: this.workingDirPath }, 110 | stdout: stdoutHandler, 111 | stderr: stderrHandler, 112 | exit: exitHandler 113 | }); 114 | 115 | proc.process.on("error", reject); 116 | }); 117 | }; 118 | 119 | GitContext.prototype.checkoutSide = function (sideName, filePath) { 120 | return new Promise((resolve, reject) => { 121 | let proc = this.runProcess({ 122 | command: this.gitCmd, 123 | args: ["checkout", `--${sideName}`, filePath], 124 | options: { cwd: this.workingDirPath }, 125 | stdout: console.log, 126 | stderr: console.error, 127 | exit: (code) => { 128 | if (code === 0) { 129 | resolve(); 130 | } else { 131 | reject(new Error(`git checkout exit: ${code}`)); 132 | } 133 | } 134 | }); 135 | 136 | proc.process.on("error", reject); 137 | }); 138 | }; 139 | 140 | GitContext.prototype.resolveFile = function (filePath) { 141 | // git-utils wants paths with forward slashes. relativize takes care of that. 142 | this.repository.repo.add(this.repository.repo.relativize(filePath)); 143 | return Promise.resolve(); 144 | }; 145 | 146 | GitContext.prototype.isRebasing = function () { 147 | let root = this.repository.getPath(); 148 | if (!root) return false; 149 | 150 | let hasDotGitDirectory = (dirName) => { 151 | let fullPath = path.join(root, dirName); 152 | let stat = fs.statSyncNoException(fullPath); 153 | return stat && stat.isDirectory; 154 | }; 155 | 156 | if (hasDotGitDirectory('rebase-apply')) return true; 157 | if (hasDotGitDirectory('rebase-merge')) return true; 158 | 159 | return false; 160 | }; 161 | 162 | GitContext.prototype.mockProcess = function (handler) { 163 | this.runProcess = handler; 164 | }; 165 | 166 | let locateGit = function () { 167 | // Use an explicitly provided path if one is available. 168 | let possiblePath = atom.config.get("merge-conflicts.gitPath"); 169 | 170 | if (possiblePath) { 171 | return Promise.resolve(possiblePath); 172 | } 173 | 174 | let search = [ 175 | 'git', // Search the inherited execution PATH. Unreliable on Macs. 176 | '/usr/local/bin/git', // Homebrew 177 | '"%PROGRAMFILES%\\Git\\bin\\git"', // Reasonable Windows default 178 | '"%LOCALAPPDATA%\\Programs\\Git\\bin\\git"' // Contributed Windows path 179 | ]; 180 | 181 | possiblePath = search.shift(); 182 | 183 | return new Promise((resolve, reject) => { 184 | let exitHandler = (code) => { 185 | if (code === 0) { 186 | return resolve(possiblePath); 187 | } 188 | 189 | errorHandler(); 190 | }; 191 | 192 | let errorHandler = (err) => { 193 | if (err) { 194 | err.handle(); 195 | 196 | // Suppress the default ENOENT handler. 197 | err.error.code = "NOTENOENT"; 198 | } 199 | 200 | possiblePath = search.shift(); 201 | 202 | if (!possiblePath) { 203 | let message = "Please set 'Git Path' correctly in the Atom settings for the Merge Conflicts" 204 | message += " package."; 205 | return reject(new Error(message)); 206 | } 207 | 208 | tryPath(); 209 | }; 210 | 211 | let tryPath = () => { 212 | new BufferedProcess({ 213 | command: possiblePath, 214 | args: ["--version"], 215 | exit: exitHandler 216 | }).onWillThrowError(errorHandler); 217 | }; 218 | 219 | tryPath(); 220 | }); 221 | }; 222 | 223 | let statusCodesFrom = function (chunk, handler) { 224 | chunk.split("\n").forEach((line) => { 225 | let m = line.match(/^(.)(.) (.+)$/); 226 | if (m) { 227 | let indexCode = m[1]; 228 | let workCode = m[2]; 229 | let p = m[3]; 230 | 231 | handler(indexCode, workCode, p); 232 | } 233 | }); 234 | }; 235 | 236 | GitContext.prototype.joinPath = function(relativePath) { 237 | return path.join(this.workingDirPath, relativePath); 238 | } 239 | 240 | GitContext.prototype.quit = function(wasRebasing) { 241 | common.quitContext(wasRebasing); 242 | } 243 | 244 | GitContext.prototype.complete = function(wasRebasing) { 245 | common.completeContext(wasRebasing); 246 | } 247 | -------------------------------------------------------------------------------- /lib/main.coffee: -------------------------------------------------------------------------------- 1 | {CompositeDisposable, Emitter} = require 'atom' 2 | 3 | {MergeConflictsView} = require './view/merge-conflicts-view' 4 | {GitOps} = require './git' 5 | 6 | pkgEmitter = null; 7 | pkgApi = null; 8 | 9 | module.exports = 10 | 11 | activate: (state) -> 12 | @subs = new CompositeDisposable 13 | @emitter = new Emitter 14 | 15 | MergeConflictsView.registerContextApi(GitOps); 16 | 17 | pkgEmitter = 18 | onDidResolveConflict: (callback) => @onDidResolveConflict(callback) 19 | didResolveConflict: (event) => @emitter.emit 'did-resolve-conflict', event 20 | onDidResolveFile: (callback) => @onDidResolveFile(callback) 21 | didResolveFile: (event) => @emitter.emit 'did-resolve-file', event 22 | onDidQuitConflictResolution: (callback) => @onDidQuitConflictResolution(callback) 23 | didQuitConflictResolution: => @emitter.emit 'did-quit-conflict-resolution' 24 | onDidCompleteConflictResolution: (callback) => @onDidCompleteConflictResolution(callback) 25 | didCompleteConflictResolution: => @emitter.emit 'did-complete-conflict-resolution' 26 | 27 | @subs.add atom.commands.add 'atom-workspace', 'merge-conflicts:detect', -> 28 | MergeConflictsView.detect(pkgEmitter) 29 | 30 | deactivate: -> 31 | @subs.dispose() 32 | @emitter.dispose() 33 | 34 | config: 35 | gitPath: 36 | type: 'string' 37 | default: '' 38 | description: 'Absolute path to your git executable.' 39 | 40 | # Invoke a callback each time that an individual conflict is resolved. 41 | # 42 | onDidResolveConflict: (callback) -> 43 | @emitter.on 'did-resolve-conflict', callback 44 | 45 | # Invoke a callback each time that a completed file is resolved. 46 | # 47 | onDidResolveFile: (callback) -> 48 | @emitter.on 'did-resolve-file', callback 49 | 50 | # Invoke a callback if conflict resolution is prematurely exited, while conflicts remain 51 | # unresolved. 52 | # 53 | onDidQuitConflictResolution: (callback) -> 54 | @emitter.on 'did-quit-conflict-resolution', callback 55 | 56 | # Invoke a callback if conflict resolution is completed successfully, with all conflicts resolved 57 | # and all files resolved. 58 | # 59 | onDidCompleteConflictResolution: (callback) -> 60 | @emitter.on 'did-complete-conflict-resolution', callback 61 | 62 | # Register a repository context provider that will have functionality for 63 | # retrieving and resolving conflicts. 64 | # 65 | registerContextApi: (contextApi) -> 66 | MergeConflictsView.registerContextApi(contextApi) 67 | 68 | 69 | showForContext: (context) -> 70 | MergeConflictsView.showForContext(context, pkgEmitter) 71 | 72 | hideForContext: (context) -> 73 | MergeConflictsView.hideForContext(context) 74 | 75 | provideApi: -> 76 | if (pkgApi == null) 77 | pkgApi = Object.freeze({ 78 | registerContextApi: @registerContextApi, 79 | showForContext: @showForContext, 80 | hideForContext: @hideForContext, 81 | onDidResolveConflict: pkgEmitter.onDidResolveConflict, 82 | onDidResolveFile: pkgEmitter.onDidResolveConflict, 83 | onDidQuitConflictResolution: pkgEmitter.onDidQuitConflictResolution, 84 | onDidCompleteConflictResolution: pkgEmitter.onDidCompleteConflictResolution, 85 | }) 86 | pkgApi 87 | -------------------------------------------------------------------------------- /lib/merge-state.coffee: -------------------------------------------------------------------------------- 1 | class MergeState 2 | 3 | constructor: (@conflicts, @context, @isRebase) -> 4 | 5 | conflictPaths: -> c.path for c in @conflicts 6 | 7 | reread: -> 8 | @context.readConflicts().then (@conflicts) => 9 | 10 | isEmpty: -> @conflicts.length is 0 11 | 12 | relativize: (filePath) -> @context.workingDirectory.relativize filePath 13 | 14 | join: (relativePath) -> @context.joinPath(relativePath) 15 | 16 | @read: (context) -> 17 | isr = context.isRebasing() 18 | context.readConflicts().then (cs) -> 19 | new MergeState(cs, context, isr) 20 | 21 | module.exports = 22 | MergeState: MergeState 23 | -------------------------------------------------------------------------------- /lib/mergetool.gitconfig: -------------------------------------------------------------------------------- 1 | [merge] 2 | tool = atom 3 | [mergetool "atom"] 4 | cmd = atom_merge $MERGED 5 | -------------------------------------------------------------------------------- /lib/navigator.coffee: -------------------------------------------------------------------------------- 1 | class Navigator 2 | 3 | constructor: (@separatorMarker) -> 4 | [@conflict, @previous, @next] = [null, null, null] 5 | 6 | linkToPrevious: (c) -> 7 | @previous = c 8 | c.navigator.next = @conflict if c? 9 | 10 | nextUnresolved: -> 11 | current = @next 12 | while current? and current.isResolved() 13 | current = current.navigator.next 14 | current 15 | 16 | previousUnresolved: -> 17 | current = @previous 18 | while current? and current.isResolved() 19 | current = current.navigator.previous 20 | current 21 | 22 | markers: -> [@separatorMarker] 23 | 24 | module.exports = 25 | Navigator: Navigator 26 | -------------------------------------------------------------------------------- /lib/side.coffee: -------------------------------------------------------------------------------- 1 | class Side 2 | constructor: (@originalText, @ref, @marker, @refBannerMarker, @position) -> 3 | @conflict = null 4 | @isDirty = false 5 | @followingMarker = null 6 | 7 | resolve: -> @conflict.resolveAs this 8 | 9 | wasChosen: -> @conflict.resolution is this 10 | 11 | lineClass: -> 12 | if @wasChosen() 13 | 'conflict-resolved' 14 | else if @isDirty 15 | 'conflict-dirty' 16 | else 17 | "conflict-#{@klass()}" 18 | 19 | markers: -> [@marker, @refBannerMarker] 20 | 21 | toString: -> 22 | text = @originalText.replace(/[\n\r]/, ' ') 23 | if text.length > 20 24 | text = text[0..17] + "..." 25 | dirtyMark = if @isDirty then ' dirty' else '' 26 | chosenMark = if @wasChosen() then ' chosen' else '' 27 | "[#{@klass()}: #{text} :#{dirtyMark}#{chosenMark}]" 28 | 29 | 30 | class OurSide extends Side 31 | 32 | site: -> 1 33 | 34 | klass: -> 'ours' 35 | 36 | description: -> 'our changes' 37 | 38 | eventName: -> 'merge-conflicts:accept-ours' 39 | 40 | class TheirSide extends Side 41 | 42 | site: -> 2 43 | 44 | klass: -> 'theirs' 45 | 46 | description: -> 'their changes' 47 | 48 | eventName: -> 'merge-conflicts:accept-theirs' 49 | 50 | class BaseSide extends Side 51 | 52 | site: -> 3 53 | 54 | klass: -> 'base' 55 | 56 | description: -> 'merged base' 57 | 58 | eventName: -> 'merge-conflicts:accept-base' 59 | 60 | module.exports = 61 | Side: Side 62 | OurSide: OurSide 63 | TheirSide: TheirSide 64 | BaseSide: BaseSide 65 | -------------------------------------------------------------------------------- /lib/view/covering-view.coffee: -------------------------------------------------------------------------------- 1 | {CompositeDisposable} = require 'atom' 2 | {View, $} = require 'space-pen' 3 | _ = require 'underscore-plus' 4 | 5 | 6 | class CoveringView extends View 7 | 8 | initialize: (@editor) -> 9 | @coverSubs = new CompositeDisposable 10 | @overlay = @editor.decorateMarker @cover(), 11 | type: 'overlay', 12 | item: this, 13 | position: 'tail' 14 | 15 | @coverSubs.add @editor.onDidDestroy => @cleanup() 16 | 17 | attached: -> 18 | view = atom.views.getView(@editor) 19 | @parent().css right: view.getVerticalScrollbarWidth() 20 | 21 | @css 'margin-top': -@editor.getLineHeightInPixels() 22 | @height @editor.getLineHeightInPixels() 23 | 24 | cleanup: -> 25 | @coverSubs.dispose() 26 | 27 | @overlay?.destroy() 28 | @overlay = null 29 | 30 | # Override to specify the marker of the first line that should be covered. 31 | cover: -> null 32 | 33 | # Override to return the Conflict that this view is responsible for. 34 | conflict: -> null 35 | 36 | isDirty: -> false 37 | 38 | # Override to determine if the content of this Side has been modified. 39 | detectDirty: -> null 40 | 41 | # Override to apply a decoration to a marker as appropriate. 42 | decorate: -> null 43 | 44 | getModel: -> null 45 | 46 | buffer: -> @editor.getBuffer() 47 | 48 | includesCursor: (cursor) -> false 49 | 50 | deleteMarker: (marker) -> 51 | @buffer().delete marker.getBufferRange() 52 | marker.destroy() 53 | 54 | scrollTo: (positionOrNull) -> 55 | @editor.setCursorBufferPosition positionOrNull if positionOrNull? 56 | 57 | prependKeystroke: (eventName, element) -> 58 | bindings = atom.keymaps.findKeyBindings command: eventName 59 | 60 | for e in bindings 61 | original = element.text() 62 | element.text(_.humanizeKeystroke(e.keystrokes) + " #{original}") 63 | 64 | module.exports = 65 | CoveringView: CoveringView 66 | -------------------------------------------------------------------------------- /lib/view/error-view.coffee: -------------------------------------------------------------------------------- 1 | {View} = require 'space-pen' 2 | 3 | class GitNotFoundErrorView extends View 4 | 5 | @content: (err) -> 6 | @div class: 'overlay from-top padded merge-conflict-error merge-conflicts-message', => 7 | @div class: 'panel', => 8 | @div class: 'panel-heading no-path', => 9 | @code 'git' 10 | @text "can't be found in any of the default locations!" 11 | @div class: 'panel-heading wrong-path', => 12 | @code 'git' 13 | @text "can't be found at " 14 | @code atom.config.get 'merge-conflicts.gitPath' 15 | @text '!' 16 | @div class: 'panel-body', => 17 | @div class: 'block', 18 | 'Please specify the correct path in the merge-conflicts package settings.' 19 | @div class: 'block', => 20 | @button class: 'btn btn-error inline-block-tight', click: 'openSettings', 'Open Settings' 21 | @button class: 'btn inline-block-tight', click: 'notRightNow', 'Not Right Now' 22 | 23 | initialize: (err) -> 24 | if atom.config.get 'merge-conflicts.gitPath' 25 | @find('.no-path').hide() 26 | @find('.wrong-path').show() 27 | else 28 | @find('.no-path').show() 29 | @find('.wrong-path').hide() 30 | 31 | openSettings: -> 32 | atom.workspace.open 'atom://config/packages' 33 | @remove() 34 | 35 | notRightNow: -> 36 | @remove() 37 | 38 | module.exports = 39 | handleErr: (err) -> 40 | return false unless err? 41 | 42 | if err.isGitError 43 | atom.workspace.addTopPanel item: new GitNotFoundErrorView(err) 44 | else 45 | atom.notifications.addError err.message 46 | console.error err.message, err.trace 47 | true 48 | -------------------------------------------------------------------------------- /lib/view/merge-conflicts-view.coffee: -------------------------------------------------------------------------------- 1 | {$, View} = require 'space-pen' 2 | {CompositeDisposable} = require 'atom' 3 | _ = require 'underscore-plus' 4 | 5 | {MergeState} = require '../merge-state' 6 | {ConflictedEditor} = require '../conflicted-editor' 7 | 8 | {ResolverView} = require './resolver-view' 9 | {handleErr} = require './error-view' 10 | 11 | class MergeConflictsView extends View 12 | 13 | @instance: null 14 | @contextApis: [] 15 | 16 | @content: (state, pkg) -> 17 | @div class: 'merge-conflicts tool-panel panel-bottom padded clearfix', => 18 | @div class: 'panel-heading', => 19 | @text 'Conflicts' 20 | @span class: 'pull-right icon icon-fold', click: 'minimize', 'Hide' 21 | @span class: 'pull-right icon icon-unfold', click: 'restore', 'Show' 22 | @div outlet: 'body', => 23 | @div class: 'conflict-list', => 24 | @ul class: 'block list-group', outlet: 'pathList', => 25 | for {path: p, message} in state.conflicts 26 | @li click: 'navigate', "data-path": p, class: 'list-item navigate', => 27 | @span class: 'inline-block icon icon-diff-modified status-modified path', p 28 | @div class: 'pull-right', => 29 | @button click: 'resolveFile', class: 'btn btn-xs btn-success inline-block-tight stage-ready', style: 'display: none', state.context.resolveText 30 | @span class: 'inline-block text-subtle', message 31 | @progress class: 'inline-block', max: 100, value: 0 32 | @span class: 'inline-block icon icon-dash staged' 33 | @div class: 'footer block pull-right', => 34 | @button class: 'btn btn-sm', click: 'quit', 'Quit' 35 | 36 | initialize: (@state, @pkg) -> 37 | @subs = new CompositeDisposable 38 | 39 | @subs.add @pkg.onDidResolveConflict (event) => 40 | p = @state.relativize event.file 41 | found = false 42 | for listElement in @pathList.children() 43 | li = $(listElement) 44 | if li.data('path') is p 45 | found = true 46 | 47 | progress = li.find('progress')[0] 48 | progress.max = event.total 49 | progress.value = event.resolved 50 | 51 | li.find('.stage-ready').show() if event.total is event.resolved 52 | 53 | unless found 54 | console.error "Unrecognized conflict path: #{p}" 55 | 56 | @subs.add @pkg.onDidResolveFile => @refresh() 57 | 58 | @subs.add atom.commands.add @element, 59 | 'merge-conflicts:entire-file-ours': @sideResolver('ours'), 60 | 'merge-conflicts:entire-file-theirs': @sideResolver('theirs') 61 | 62 | navigate: (event, element) -> 63 | repoPath = element.find(".path").text() 64 | fullPath = @state.join repoPath 65 | atom.workspace.open(fullPath) 66 | 67 | minimize: -> 68 | @addClass 'minimized' 69 | @body.hide 'fast' 70 | 71 | restore: -> 72 | @removeClass 'minimized' 73 | @body.show 'fast' 74 | 75 | quit: -> 76 | @pkg.didQuitConflictResolution() 77 | @finish() 78 | @state.context.quit(@state.isRebase) 79 | 80 | refresh: -> 81 | @state.reread().catch(handleErr).then => 82 | # Any files that were present, but aren't there any more, have been resolved. 83 | for item in @pathList.find('li') 84 | p = $(item).data('path') 85 | icon = $(item).find('.staged') 86 | icon.removeClass 'icon-dash icon-check text-success' 87 | if _.contains @state.conflictPaths(), p 88 | icon.addClass 'icon-dash' 89 | else 90 | icon.addClass 'icon-check text-success' 91 | @pathList.find("li[data-path='#{p}'] .stage-ready").hide() 92 | 93 | return unless @state.isEmpty() 94 | @pkg.didCompleteConflictResolution() 95 | @finish() 96 | @state.context.complete(@state.isRebase) 97 | 98 | finish: -> 99 | @subs.dispose() 100 | @hide 'fast', => 101 | MergeConflictsView.instance = null 102 | @remove() 103 | 104 | sideResolver: (side) -> 105 | (event) => 106 | p = $(event.target).closest('li').data('path') 107 | @state.context.checkoutSide(side, p) 108 | .then => 109 | full = @state.join p 110 | @pkg.didResolveConflict file: full, total: 1, resolved: 1 111 | atom.workspace.open p 112 | .catch (err) -> 113 | handleErr(err) 114 | 115 | resolveFile: (event, element) -> 116 | repoPath = element.closest('li').data('path') 117 | filePath = @state.join repoPath 118 | 119 | for e in atom.workspace.getTextEditors() 120 | e.save() if e.getPath() is filePath 121 | 122 | @state.context.resolveFile(repoPath) 123 | .then => 124 | @pkg.didResolveFile file: filePath 125 | .catch (err) -> 126 | handleErr(err) 127 | 128 | @registerContextApi: (contextApi) -> 129 | @contextApis.push(contextApi) 130 | 131 | @showForContext: (context, pkg) -> 132 | if @instance 133 | @instance.finish() 134 | MergeState.read(context).then (state) => 135 | return if state.isEmpty() 136 | @openForState(state, pkg) 137 | .catch handleErr 138 | 139 | @hideForContext: (context) -> 140 | return unless @instance 141 | return unless @instance.state.context == context 142 | @instance.finish() 143 | 144 | @detect: (pkg) -> 145 | return if @instance? 146 | 147 | Promise.all(@contextApis.map (contextApi) => contextApi.getContext()) 148 | .then (contexts) => 149 | # filter out nulls and take the highest priority context. 150 | Promise.all( 151 | _.filter(contexts, Boolean) 152 | .sort (context1, context2) => context2.priority - context1.priority 153 | .map (context) => MergeState.read context 154 | ) 155 | .then (states) => 156 | state = states.find (state) -> not state.isEmpty() 157 | unless state? 158 | atom.notifications.addInfo "Nothing to Merge", 159 | detail: "No conflicts here!", 160 | dismissable: true 161 | return 162 | @openForState(state, pkg) 163 | .catch handleErr 164 | 165 | @openForState: (state, pkg) -> 166 | view = new MergeConflictsView(state, pkg) 167 | @instance = view 168 | atom.workspace.addBottomPanel item: view 169 | 170 | @instance.subs.add atom.workspace.observeTextEditors (editor) => 171 | @markConflictsIn state, editor, pkg 172 | 173 | @markConflictsIn: (state, editor, pkg) -> 174 | return if state.isEmpty() 175 | 176 | fullPath = editor.getPath() 177 | repoPath = state.relativize fullPath 178 | return unless repoPath? 179 | 180 | return unless _.contains state.conflictPaths(), repoPath 181 | 182 | e = new ConflictedEditor(state, pkg, editor) 183 | e.mark() 184 | 185 | 186 | module.exports = 187 | MergeConflictsView: MergeConflictsView 188 | -------------------------------------------------------------------------------- /lib/view/navigation-view.coffee: -------------------------------------------------------------------------------- 1 | {CompositeDisposable} = require 'atom' 2 | {CoveringView} = require './covering-view' 3 | 4 | class NavigationView extends CoveringView 5 | 6 | @content: (navigator, editor) -> 7 | @div class: 'controls navigation', => 8 | @text ' ' 9 | @span class: 'pull-right', => 10 | @button class: 'btn btn-xs', click: 'up', outlet: 'prevBtn', 'prev' 11 | @button class: 'btn btn-xs', click: 'down', outlet: 'nextBtn', 'next' 12 | 13 | initialize: (@navigator, editor) -> 14 | @subs = new CompositeDisposable 15 | 16 | super editor 17 | 18 | @prependKeystroke 'merge-conflicts:previous-unresolved', @prevBtn 19 | @prependKeystroke 'merge-conflicts:next-unresolved', @nextBtn 20 | 21 | @subs.add @navigator.conflict.onDidResolveConflict => 22 | @deleteMarker @cover() 23 | @remove() 24 | @cleanup() 25 | 26 | cleanup: -> 27 | super 28 | @subs.dispose() 29 | 30 | cover: -> @navigator.separatorMarker 31 | 32 | up: -> @scrollTo @navigator.previousUnresolved()?.scrollTarget() 33 | 34 | down: -> @scrollTo @navigator.nextUnresolved()?.scrollTarget() 35 | 36 | conflict: -> @navigator.conflict 37 | 38 | toString: -> "{NavView of: #{@conflict()}}" 39 | 40 | module.exports = 41 | NavigationView: NavigationView 42 | -------------------------------------------------------------------------------- /lib/view/resolver-view.coffee: -------------------------------------------------------------------------------- 1 | {CompositeDisposable} = require 'atom' 2 | {View} = require 'space-pen' 3 | 4 | {handleErr} = require './error-view' 5 | 6 | class ResolverView extends View 7 | 8 | @content: (editor, state, pkg) -> 9 | resolveText = state.context.resolveText 10 | @div class: 'overlay from-top resolver', => 11 | @div class: 'block text-highlight', "We're done here" 12 | @div class: 'block', => 13 | @div class: 'block text-info', => 14 | @text "You've dealt with all of the conflicts in this file." 15 | @div class: 'block text-info', => 16 | @span outlet: 'actionText', "Save and #{resolveText}" 17 | @text ' this file?' 18 | @div class: 'pull-left', => 19 | @button class: 'btn btn-primary', click: 'dismiss', 'Maybe Later' 20 | @div class: 'pull-right', => 21 | @button class: 'btn btn-primary', click: 'resolve', resolveText 22 | 23 | initialize: (@editor, @state, @pkg) -> 24 | @subs = new CompositeDisposable() 25 | 26 | @refresh() 27 | @subs.add @editor.onDidSave => @refresh() 28 | 29 | @subs.add atom.commands.add @element, 'merge-conflicts:quit', => @dismiss() 30 | 31 | detached: -> @subs.dispose() 32 | 33 | getModel: -> null 34 | 35 | relativePath: -> 36 | @state.relativize @editor.getURI() 37 | 38 | refresh: -> 39 | @state.context.isResolvedFile @relativePath() 40 | .then (resolved) => 41 | modified = @editor.isModified() 42 | 43 | needsSaved = modified 44 | needsResolve = modified or not resolved 45 | 46 | unless needsSaved or needsResolve 47 | @hide 'fast', => @remove() 48 | @pkg.didResolveFile file: @editor.getURI() 49 | return 50 | 51 | resolveText = @state.context.resolveText 52 | if needsSaved 53 | @actionText.text "Save and #{resolveText.toLowerCase()}" 54 | else if needsResolve 55 | @actionText.text resolveText 56 | .catch handleErr 57 | 58 | resolve: -> 59 | # Suport async save implementations. 60 | Promise.resolve(@editor.save()).then => 61 | @state.context.resolveFile @relativePath() 62 | .then => 63 | @refresh() 64 | .catch handleErr 65 | 66 | dismiss: -> 67 | @hide 'fast', => @remove() 68 | 69 | module.exports = 70 | ResolverView: ResolverView 71 | -------------------------------------------------------------------------------- /lib/view/side-view.coffee: -------------------------------------------------------------------------------- 1 | {CompositeDisposable} = require 'atom' 2 | {CoveringView} = require './covering-view' 3 | 4 | class SideView extends CoveringView 5 | 6 | @content: (side, editor) -> 7 | @div class: "side #{side.klass()} #{side.position} ui-site-#{side.site()}", => 8 | @div class: 'controls', => 9 | @label class: 'text-highlight', side.ref 10 | @span class: 'text-subtle', "// #{side.description()}" 11 | @span class: 'pull-right', => 12 | @button class: 'btn btn-xs inline-block-tight revert', click: 'revert', outlet: 'revertBtn', 'Revert' 13 | @button class: 'btn btn-xs inline-block-tight', click: 'useMe', outlet: 'useMeBtn', 'Use Me' 14 | 15 | initialize: (@side, editor) -> 16 | @subs = new CompositeDisposable 17 | @decoration = null 18 | 19 | super editor 20 | 21 | @detectDirty() 22 | @prependKeystroke @side.eventName(), @useMeBtn 23 | @prependKeystroke 'merge-conflicts:revert-current', @revertBtn 24 | 25 | attached: -> 26 | super 27 | 28 | @decorate() 29 | @subs.add @side.conflict.onDidResolveConflict => 30 | @deleteMarker @side.refBannerMarker 31 | @deleteMarker @side.marker unless @side.wasChosen() 32 | @remove() 33 | @cleanup() 34 | 35 | cleanup: -> 36 | super 37 | @subs.dispose() 38 | 39 | cover: -> @side.refBannerMarker 40 | 41 | decorate: -> 42 | @decoration?.destroy() 43 | 44 | return if @side.conflict.isResolved() && !@side.wasChosen() 45 | 46 | args = 47 | type: 'line' 48 | class: @side.lineClass() 49 | @decoration = @editor.decorateMarker(@side.marker, args) 50 | 51 | conflict: -> @side.conflict 52 | 53 | isDirty: -> @side.isDirty 54 | 55 | includesCursor: (cursor) -> 56 | m = @side.marker 57 | [h, t] = [m.getHeadBufferPosition(), m.getTailBufferPosition()] 58 | p = cursor.getBufferPosition() 59 | t.isLessThanOrEqual(p) and h.isGreaterThanOrEqual(p) 60 | 61 | useMe: -> 62 | @editor.transact => 63 | @side.resolve() 64 | @decorate() 65 | 66 | revert: -> 67 | @editor.setTextInBufferRange @side.marker.getBufferRange(), @side.originalText 68 | @decorate() 69 | 70 | detectDirty: -> 71 | currentText = @editor.getTextInBufferRange @side.marker.getBufferRange() 72 | @side.isDirty = currentText isnt @side.originalText 73 | 74 | @decorate() 75 | 76 | @removeClass 'dirty' 77 | @addClass 'dirty' if @side.isDirty 78 | 79 | toString: -> "{SideView of: #{@side}}" 80 | 81 | module.exports = 82 | SideView: SideView 83 | -------------------------------------------------------------------------------- /menus/merge-conflicts.cson: -------------------------------------------------------------------------------- 1 | # See https://atom.io/docs/latest/creating-a-package#menus for more details 2 | 'context-menu': 3 | 'atom-text-editor.conflicted': [{ 4 | label: 'Resolve Conflict', 5 | submenu: [ 6 | { label: 'Current', command: 'merge-conflicts:accept-current' } 7 | { label: 'Ours', command: 'merge-conflicts:accept-ours' } 8 | { label: 'Theirs', command: 'merge-conflicts:accept-theirs' } 9 | { label: 'Ours Then Theirs', command: 'merge-conflicts:ours-then-theirs' } 10 | { label: 'Theirs Then Ours', command: 'merge-conflicts:theirs-then-ours' } 11 | ] 12 | }] 13 | '.merge-conflicts .navigate': [ 14 | { label: 'Resolve Entire File Ours', command: 'merge-conflicts:entire-file-ours' } 15 | { label: 'Resolve Entire File Theirs', command: 'merge-conflicts:entire-file-theirs' } 16 | ] 17 | 18 | 'menu': [{ 19 | label: 'Packages' 20 | submenu: [ 21 | label: 'Merge Conflicts', 22 | submenu: [{ label: 'Detect', command: 'merge-conflicts:detect' }] 23 | ] 24 | }] 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "merge-conflicts", 3 | "main": "./lib/main", 4 | "version": "1.4.5", 5 | "private": true, 6 | "description": "Resolve git conflicts within Atom", 7 | "contributors": [ 8 | "Maximilian Schüßler " 9 | ], 10 | "repository": "https://github.com/smashwilson/merge-conflicts", 11 | "license": "MIT", 12 | "engines": { 13 | "atom": ">=0.185.0 <2.0.0" 14 | }, 15 | "dependencies": { 16 | "space-pen": "^5.0.1", 17 | "underscore-plus": "1.x" 18 | }, 19 | "providedServices": { 20 | "merge-conflicts": { 21 | "description": "Register conflict resolver integrations", 22 | "versions": { 23 | "0.0.0": "provideApi" 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /script/test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | apm test 4 | -------------------------------------------------------------------------------- /spec/conflict-spec.coffee: -------------------------------------------------------------------------------- 1 | {Conflict} = require '../lib/conflict' 2 | util = require './util' 3 | 4 | describe "Conflict", -> 5 | 6 | describe 'a single two-way diff', -> 7 | [conflict] = [] 8 | 9 | beforeEach -> 10 | util.openPath 'single-2way-diff.txt', (editorView) -> 11 | conflict = Conflict.all({ isRebase: false }, editorView.getModel())[0] 12 | 13 | it 'identifies the correct rows', -> 14 | expect(util.rowRangeFrom conflict.ours.marker).toEqual([1, 2]) 15 | expect(conflict.ours.ref).toBe('HEAD') 16 | expect(util.rowRangeFrom conflict.theirs.marker).toEqual([3, 4]) 17 | expect(conflict.theirs.ref).toBe('master') 18 | 19 | it 'finds the ref banners', -> 20 | expect(util.rowRangeFrom conflict.ours.refBannerMarker).toEqual([0, 1]) 21 | expect(util.rowRangeFrom conflict.theirs.refBannerMarker).toEqual([4, 5]) 22 | 23 | it 'finds the separator', -> 24 | expect(util.rowRangeFrom conflict.navigator.separatorMarker).toEqual([2, 3]) 25 | 26 | it 'marks "ours" as the top and "theirs" as the bottom', -> 27 | expect(conflict.ours.position).toBe('top') 28 | expect(conflict.theirs.position).toBe('bottom') 29 | 30 | it 'links each side to the following marker', -> 31 | expect(conflict.ours.followingMarker).toBe(conflict.navigator.separatorMarker) 32 | expect(conflict.theirs.followingMarker).toBe(conflict.theirs.refBannerMarker) 33 | 34 | it 'does not have base side', -> 35 | expect(conflict.base).toBeNull() 36 | 37 | describe 'a single three-way diff', -> 38 | [conflict] = [] 39 | 40 | beforeEach -> 41 | util.openPath 'single-3way-diff.txt', (editorView) -> 42 | conflict = Conflict.all({ isRebase: false }, editorView.getModel())[0] 43 | 44 | it 'identifies the correct rows', -> 45 | expect(util.rowRangeFrom conflict.ours.marker).toEqual([1, 2]) 46 | expect(conflict.ours.ref).toBe('HEAD') 47 | expect(util.rowRangeFrom conflict.base.marker).toEqual([3, 4]) 48 | expect(conflict.base.ref).toBe('merged common ancestors') 49 | expect(util.rowRangeFrom conflict.theirs.marker).toEqual([5, 6]) 50 | expect(conflict.theirs.ref).toBe('master') 51 | 52 | it 'finds the ref banners', -> 53 | expect(util.rowRangeFrom conflict.ours.refBannerMarker).toEqual([0, 1]) 54 | expect(util.rowRangeFrom conflict.base.refBannerMarker).toEqual([2, 3]) 55 | expect(util.rowRangeFrom conflict.theirs.refBannerMarker).toEqual([6, 7]) 56 | 57 | it 'finds the separator', -> 58 | expect(util.rowRangeFrom conflict.navigator.separatorMarker).toEqual([4, 5]) 59 | 60 | it 'marks "ours" as the top and "theirs" as the bottom', -> 61 | expect(conflict.ours.position).toBe('top') 62 | expect(conflict.base.position).toBe('base') 63 | expect(conflict.theirs.position).toBe('bottom') 64 | 65 | it 'links each side to the following marker', -> 66 | expect(conflict.ours.followingMarker).toBe(conflict.base.refBannerMarker) 67 | expect(conflict.base.followingMarker).toBe(conflict.navigator.separatorMarker) 68 | expect(conflict.theirs.followingMarker).toBe(conflict.theirs.refBannerMarker) 69 | 70 | it "identifies the correct rows for complex three-way diff", -> 71 | util.openPath 'single-3way-diff-complex.txt', (editorView) -> 72 | conflict = Conflict.all({ isRebase: false }, editorView.getModel())[0] 73 | expect(util.rowRangeFrom conflict.ours.marker).toEqual([1, 2]) 74 | expect(conflict.ours.ref).toBe('HEAD') 75 | expect(util.rowRangeFrom conflict.base.marker).toEqual([3, 18]) 76 | expect(conflict.base.ref).toBe('merged common ancestors') 77 | expect(util.rowRangeFrom conflict.theirs.marker).toEqual([19, 20]) 78 | expect(conflict.theirs.ref).toBe('master') 79 | 80 | it "finds multiple conflict markings", -> 81 | util.openPath 'multi-2way-diff.txt', (editorView) -> 82 | cs = Conflict.all({}, editorView.getModel()) 83 | 84 | expect(cs.length).toBe(2) 85 | expect(util.rowRangeFrom cs[0].ours.marker).toEqual([5, 7]) 86 | expect(util.rowRangeFrom cs[0].theirs.marker).toEqual([8, 9]) 87 | expect(util.rowRangeFrom cs[1].ours.marker).toEqual([14, 15]) 88 | expect(util.rowRangeFrom cs[1].theirs.marker).toEqual([16, 17]) 89 | 90 | describe 'with corrupted diffs', -> 91 | 92 | it 'handles corrupted diff output', -> 93 | util.openPath 'corrupted-2way-diff.txt', (editorView) -> 94 | cs = Conflict.all({}, editorView.getModel()) 95 | expect(cs.length).toBe(0) 96 | 97 | it 'handles corrupted diff3 output', -> 98 | util.openPath 'corrupted-3way-diff.txt', (editorView) -> 99 | cs = Conflict.all({}, editorView.getModel()) 100 | 101 | expect(cs.length).toBe(1) 102 | expect(util.rowRangeFrom cs[0].ours.marker).toEqual([13, 14]) 103 | expect(util.rowRangeFrom cs[0].base.marker).toEqual([15, 16]) 104 | expect(util.rowRangeFrom cs[0].theirs.marker).toEqual([17, 18]) 105 | 106 | describe 'when rebasing', -> 107 | [conflict] = [] 108 | 109 | beforeEach -> 110 | util.openPath 'rebase-2way-diff.txt', (editorView) -> 111 | conflict = Conflict.all({ isRebase: true }, editorView.getModel())[0] 112 | 113 | it 'swaps the lines for "ours" and "theirs"', -> 114 | expect(util.rowRangeFrom conflict.theirs.marker).toEqual([3, 4]) 115 | expect(util.rowRangeFrom conflict.ours.marker).toEqual([5, 6]) 116 | 117 | it 'recognizes banner lines with commit shortlog messages', -> 118 | expect(util.rowRangeFrom conflict.theirs.refBannerMarker).toEqual([2, 3]) 119 | expect(util.rowRangeFrom conflict.ours.refBannerMarker).toEqual([6, 7]) 120 | 121 | it 'marks "theirs" as the top and "ours" as the bottom', -> 122 | expect(conflict.theirs.position).toBe('top') 123 | expect(conflict.ours.position).toBe('bottom') 124 | 125 | it 'links each side to the following marker', -> 126 | expect(conflict.theirs.followingMarker).toBe(conflict.navigator.separatorMarker) 127 | expect(conflict.ours.followingMarker).toBe(conflict.ours.refBannerMarker) 128 | 129 | describe 'sides', -> 130 | [editor, conflict] = [] 131 | 132 | beforeEach -> 133 | util.openPath 'single-2way-diff.txt', (editorView) -> 134 | editor = editorView.getModel() 135 | [conflict] = Conflict.all {}, editor 136 | 137 | it 'retains a reference to conflict', -> 138 | expect(conflict.ours.conflict).toBe(conflict) 139 | expect(conflict.theirs.conflict).toBe(conflict) 140 | 141 | it 'remembers its initial text', -> 142 | editor.setCursorBufferPosition [1, 0] 143 | editor.insertText "I prefer this text! " 144 | 145 | expect(conflict.ours.originalText).toBe("These are my changes\n") 146 | 147 | it 'resolves as "ours"', -> 148 | conflict.ours.resolve() 149 | 150 | expect(conflict.resolution).toBe(conflict.ours) 151 | expect(conflict.ours.wasChosen()).toBe(true) 152 | expect(conflict.theirs.wasChosen()).toBe(false) 153 | 154 | it 'resolves as "theirs"', -> 155 | conflict.theirs.resolve() 156 | 157 | expect(conflict.resolution).toBe(conflict.theirs) 158 | expect(conflict.ours.wasChosen()).toBe(false) 159 | expect(conflict.theirs.wasChosen()).toBe(true) 160 | 161 | it 'broadcasts an event on resolution', -> 162 | resolved = false 163 | conflict.onDidResolveConflict -> resolved = true 164 | conflict.ours.resolve() 165 | expect(resolved).toBe(true) 166 | 167 | describe 'navigator', -> 168 | [conflicts, navigator] = [] 169 | 170 | beforeEach -> 171 | util.openPath 'triple-2way-diff.txt', (editorView) -> 172 | conflicts = Conflict.all({}, editorView.getModel()) 173 | navigator = conflicts[1].navigator 174 | 175 | it 'knows its conflict', -> 176 | expect(navigator.conflict).toBe(conflicts[1]) 177 | 178 | it 'links to the previous conflict', -> 179 | expect(navigator.previous).toBe(conflicts[0]) 180 | 181 | it 'links to the next conflict', -> 182 | expect(navigator.next).toBe(conflicts[2]) 183 | 184 | it 'skips resolved conflicts', -> 185 | nav = conflicts[0].navigator 186 | conflicts[1].ours.resolve() 187 | expect(nav.nextUnresolved()).toBe(conflicts[2]) 188 | 189 | it 'returns null at the end', -> 190 | nav = conflicts[2].navigator 191 | expect(nav.next).toBeNull() 192 | expect(nav.nextUnresolved()).toBeNull() 193 | -------------------------------------------------------------------------------- /spec/conflicted-editor-spec.coffee: -------------------------------------------------------------------------------- 1 | {$} = require 'space-pen' 2 | _ = require 'underscore-plus' 3 | 4 | {ConflictedEditor} = require '../lib/conflicted-editor' 5 | {GitOps} = require '../lib/git' 6 | util = require './util' 7 | 8 | describe 'ConflictedEditor', -> 9 | [editorView, editor, state, m, pkg] = [] 10 | 11 | cursors = -> c.getBufferPosition().toArray() for c in editor.getCursors() 12 | 13 | detectDirty = -> 14 | for sv in m.coveringViews 15 | sv.detectDirty() if 'detectDirty' of sv 16 | 17 | linesForMarker = (marker) -> 18 | fromBuffer = marker.getTailBufferPosition() 19 | fromScreen = editor.screenPositionForBufferPosition fromBuffer 20 | toBuffer = marker.getHeadBufferPosition() 21 | toScreen = editor.screenPositionForBufferPosition toBuffer 22 | 23 | result = $() 24 | for row in _.range(fromScreen.row, toScreen.row) 25 | result = result.add editorView.component.lineNodeForScreenRow(row) 26 | result 27 | 28 | beforeEach -> 29 | pkg = util.pkgEmitter() 30 | 31 | afterEach -> 32 | pkg.dispose() 33 | 34 | m?.cleanup() 35 | 36 | describe 'with a merge conflict', -> 37 | 38 | beforeEach -> 39 | util.openPath "triple-2way-diff.txt", (v) -> 40 | editorView = v 41 | editorView.getFirstVisibleScreenRow = -> 0 42 | editorView.getLastVisibleScreenRow = -> 999 43 | 44 | editor = editorView.getModel() 45 | state = 46 | isRebase: false 47 | relativize: (filepath) -> filepath 48 | context: 49 | isResolvedFile: (filepath) -> Promise.resolve false 50 | 51 | m = new ConflictedEditor(state, pkg, editor) 52 | m.mark() 53 | 54 | it 'attaches two SideViews and a NavigationView for each conflict', -> 55 | expect($(editorView).find('.side').length).toBe(6) 56 | expect($(editorView).find('.navigation').length).toBe(3) 57 | 58 | it 'locates the correct lines', -> 59 | lines = linesForMarker m.conflicts[1].ours.marker 60 | expect(lines.text()).toBe("My middle changes") 61 | 62 | it 'applies the "ours" class to our sides of conflicts', -> 63 | lines = linesForMarker m.conflicts[0].ours.marker 64 | expect(lines.hasClass 'conflict-ours').toBe(true) 65 | 66 | it 'applies the "theirs" class to their sides of conflicts', -> 67 | lines = linesForMarker m.conflicts[0].theirs.marker 68 | expect(lines.hasClass 'conflict-theirs').toBe(true) 69 | 70 | it 'applies the "dirty" class to modified sides', -> 71 | editor.setCursorBufferPosition [14, 0] 72 | editor.insertText "Make conflict 1 dirty" 73 | detectDirty() 74 | 75 | lines = linesForMarker m.conflicts[1].ours.marker 76 | expect(lines.hasClass 'conflict-dirty').toBe(true) 77 | expect(lines.hasClass 'conflict-ours').toBe(false) 78 | 79 | it 'broadcasts the onDidResolveConflict event', -> 80 | event = null 81 | pkg.onDidResolveConflict (e) -> event = e 82 | m.conflicts[2].theirs.resolve() 83 | 84 | expect(event.file).toBe(editor.getPath()) 85 | expect(event.total).toBe(3) 86 | expect(event.resolved).toBe(1) 87 | expect(event.source).toBe(m) 88 | 89 | it 'tracks the active conflict side', -> 90 | editor.setCursorBufferPosition [11, 0] 91 | expect(m.active()).toEqual([]) 92 | editor.setCursorBufferPosition [14, 5] 93 | expect(m.active()).toEqual([m.conflicts[1].ours]) 94 | 95 | describe 'with an active merge conflict', -> 96 | [active] = [] 97 | 98 | beforeEach -> 99 | editor.setCursorBufferPosition [14, 5] 100 | active = m.conflicts[1] 101 | 102 | it 'accepts the current side with merge-conflicts:accept-current', -> 103 | atom.commands.dispatch editorView, 'merge-conflicts:accept-current' 104 | expect(active.resolution).toBe(active.ours) 105 | 106 | it "does nothing if you have cursors in both sides", -> 107 | editor.addCursorAtBufferPosition [16, 2] 108 | atom.commands.dispatch editorView, 'merge-conflicts:accept-current' 109 | expect(active.resolution).toBeNull() 110 | 111 | it 'accepts "ours" on merge-conflicts:accept-ours', -> 112 | atom.commands.dispatch editorView, 'merge-conflicts:accept-current' 113 | expect(active.resolution).toBe(active.ours) 114 | 115 | it 'accepts "theirs" on merge-conflicts:accept-theirs', -> 116 | atom.commands.dispatch editorView, 'merge-conflicts:accept-theirs' 117 | expect(active.resolution).toBe(active.theirs) 118 | 119 | it 'jumps to the next unresolved on merge-conflicts:next-unresolved', -> 120 | atom.commands.dispatch editorView, 'merge-conflicts:next-unresolved' 121 | expect(cursors()).toEqual([[22, 0]]) 122 | 123 | it 'jumps to the previous unresolved on merge-conflicts:previous-unresolved', -> 124 | atom.commands.dispatch editorView, 'merge-conflicts:previous-unresolved' 125 | expect(cursors()).toEqual([[5, 0]]) 126 | 127 | it 'reverts a dirty hunk on merge-conflicts:revert-current', -> 128 | editor.insertText 'this is a change' 129 | detectDirty() 130 | expect(active.ours.isDirty).toBe(true) 131 | 132 | atom.commands.dispatch editorView, 'merge-conflicts:revert-current' 133 | detectDirty() 134 | expect(active.ours.isDirty).toBe(false) 135 | 136 | it 'accepts ours-then-theirs on merge-conflicts:ours-then-theirs', -> 137 | atom.commands.dispatch editorView, 'merge-conflicts:ours-then-theirs' 138 | expect(active.resolution).toBe(active.ours) 139 | t = editor.getTextInBufferRange active.resolution.marker.getBufferRange() 140 | expect(t).toBe("My middle changes\nYour middle changes\n") 141 | 142 | it 'accepts theirs-then-ours on merge-conflicts:theirs-then-ours', -> 143 | atom.commands.dispatch editorView, 'merge-conflicts:theirs-then-ours' 144 | expect(active.resolution).toBe(active.theirs) 145 | t = editor.getTextInBufferRange active.resolution.marker.getBufferRange() 146 | expect(t).toBe("Your middle changes\nMy middle changes\n") 147 | 148 | describe 'without an active conflict', -> 149 | 150 | beforeEach -> 151 | editor.setCursorBufferPosition [11, 6] 152 | 153 | it 'no-ops the resolution commands', -> 154 | for e in ['accept-current', 'accept-ours', 'accept-theirs', 'revert-current'] 155 | atom.commands.dispatch editorView, "merge-conflicts:#{e}" 156 | expect(m.active()).toEqual([]) 157 | for c in m.conflicts 158 | expect(c.isResolved()).toBe(false) 159 | 160 | it 'jumps to the next unresolved on merge-conflicts:next-unresolved', -> 161 | expect(m.active()).toEqual([]) 162 | atom.commands.dispatch editorView, 'merge-conflicts:next-unresolved' 163 | expect(cursors()).toEqual([[14, 0]]) 164 | 165 | it 'jumps to the previous unresolved on merge-conflicts:next-unresolved', -> 166 | atom.commands.dispatch editorView, 'merge-conflicts:previous-unresolved' 167 | expect(cursors()).toEqual([[5, 0]]) 168 | 169 | describe 'when the resolution is complete', -> 170 | 171 | beforeEach -> c.ours.resolve() for c in m.conflicts 172 | 173 | it 'removes all of the CoveringViews', -> 174 | expect($(editorView).find('.overlayer .side').length).toBe(0) 175 | expect($(editorView).find('.overlayer .navigation').length).toBe(0) 176 | 177 | it 'appends a ResolverView to the workspace', -> 178 | workspaceView = atom.views.getView atom.workspace 179 | expect($(workspaceView).find('.resolver').length).toBe(1) 180 | 181 | describe 'when all resolutions are complete', -> 182 | 183 | beforeEach -> 184 | c.theirs.resolve() for c in m.conflicts 185 | pkg.didCompleteConflictResolution() 186 | 187 | it 'destroys all Conflict markers', -> 188 | for c in m.conflicts 189 | for marker in c.markers() 190 | expect(marker.isDestroyed()).toBe(true) 191 | 192 | it 'removes the .conflicted class', -> 193 | expect($(editorView).hasClass 'conflicted').toBe(false) 194 | 195 | describe 'with a rebase conflict', -> 196 | [active] = [] 197 | 198 | beforeEach -> 199 | util.openPath "rebase-2way-diff.txt", (v) -> 200 | editorView = v 201 | editorView.getFirstVisibleScreenRow = -> 0 202 | editorView.getLastVisibleScreenRow = -> 999 203 | 204 | editor = editorView.getModel() 205 | state = 206 | isRebase: true 207 | relativize: (filepath) -> filepath 208 | context: 209 | isResolvedFile: -> Promise.resolve(false) 210 | 211 | m = new ConflictedEditor(state, pkg, editor) 212 | m.mark() 213 | 214 | editor.setCursorBufferPosition [3, 14] 215 | active = m.conflicts[0] 216 | 217 | it 'accepts theirs-then-ours on merge-conflicts:theirs-then-ours', -> 218 | atom.commands.dispatch editorView, 'merge-conflicts:theirs-then-ours' 219 | expect(active.resolution).toBe(active.theirs) 220 | t = editor.getTextInBufferRange active.resolution.marker.getBufferRange() 221 | expect(t).toBe("These are your changes\nThese are my changes\n") 222 | 223 | it 'accepts ours-then-theirs on merge-conflicts:ours-then-theirs', -> 224 | atom.commands.dispatch editorView, 'merge-conflicts:ours-then-theirs' 225 | expect(active.resolution).toBe(active.ours) 226 | t = editor.getTextInBufferRange active.resolution.marker.getBufferRange() 227 | expect(t).toBe("These are my changes\nThese are your changes\n") 228 | -------------------------------------------------------------------------------- /spec/fixtures/corrupted-2way-diff.txt: -------------------------------------------------------------------------------- 1 | <<<<<<< HEAD 2 | These are my changes 3 | ======= 4 | These are your changes 5 | 6 | Oops, deleted the end marker. 7 | -------------------------------------------------------------------------------- /spec/fixtures/corrupted-3way-diff.txt: -------------------------------------------------------------------------------- 1 | This is a file containing a corrupted diff3 patch. 2 | 3 | <<<<<<< HEAD 4 | These are my changes 5 | ||||||| merged common ancestors 6 | These are original texts 7 | Oops, we never get a separator 8 | These are your changes 9 | >>>>>>> master 10 | 11 | It also contains a valid one. 12 | 13 | <<<<<<< HEAD 14 | These are my changes 15 | ||||||| merged common ancestors 16 | These are original texts 17 | ======= 18 | These are your changes 19 | >>>>>>> master 20 | 21 | And some text after the end. 22 | -------------------------------------------------------------------------------- /spec/fixtures/irebasing.git/COMMIT_EDITMSG: -------------------------------------------------------------------------------- 1 | subdir 2 | -------------------------------------------------------------------------------- /spec/fixtures/irebasing.git/HEAD: -------------------------------------------------------------------------------- 1 | 51d48cc54f3f108ed59c0c4d7470a81a6cb4896a 2 | -------------------------------------------------------------------------------- /spec/fixtures/irebasing.git/MERGE_MSG: -------------------------------------------------------------------------------- 1 | aaaa 2 | 3 | Conflicts: 4 | file 5 | -------------------------------------------------------------------------------- /spec/fixtures/irebasing.git/ORIG_HEAD: -------------------------------------------------------------------------------- 1 | 6806d7673c7e5802dc49e7a4aa3a6cd683d72eb0 2 | -------------------------------------------------------------------------------- /spec/fixtures/irebasing.git/config: -------------------------------------------------------------------------------- 1 | [core] 2 | repositoryformatversion = 0 3 | filemode = true 4 | bare = false 5 | logallrefupdates = true 6 | ignorecase = true 7 | precomposeunicode = false 8 | -------------------------------------------------------------------------------- /spec/fixtures/irebasing.git/description: -------------------------------------------------------------------------------- 1 | Unnamed repository; edit this file 'description' to name the repository. 2 | -------------------------------------------------------------------------------- /spec/fixtures/irebasing.git/hooks/applypatch-msg.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to check the commit log message taken by 4 | # applypatch from an e-mail message. 5 | # 6 | # The hook should exit with non-zero status after issuing an 7 | # appropriate message if it wants to stop the commit. The hook is 8 | # allowed to edit the commit message file. 9 | # 10 | # To enable this hook, rename this file to "applypatch-msg". 11 | 12 | . git-sh-setup 13 | test -x "$GIT_DIR/hooks/commit-msg" && 14 | exec "$GIT_DIR/hooks/commit-msg" ${1+"$@"} 15 | : 16 | -------------------------------------------------------------------------------- /spec/fixtures/irebasing.git/hooks/commit-msg.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to check the commit log message. 4 | # Called by "git commit" with one argument, the name of the file 5 | # that has the commit message. The hook should exit with non-zero 6 | # status after issuing an appropriate message if it wants to stop the 7 | # commit. The hook is allowed to edit the commit message file. 8 | # 9 | # To enable this hook, rename this file to "commit-msg". 10 | 11 | # Uncomment the below to add a Signed-off-by line to the message. 12 | # Doing this in a hook is a bad idea in general, but the prepare-commit-msg 13 | # hook is more suited to it. 14 | # 15 | # SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') 16 | # grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" 17 | 18 | # This example catches duplicate Signed-off-by lines. 19 | 20 | test "" = "$(grep '^Signed-off-by: ' "$1" | 21 | sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || { 22 | echo >&2 Duplicate Signed-off-by lines. 23 | exit 1 24 | } 25 | -------------------------------------------------------------------------------- /spec/fixtures/irebasing.git/hooks/post-update.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to prepare a packed repository for use over 4 | # dumb transports. 5 | # 6 | # To enable this hook, rename this file to "post-update". 7 | 8 | exec git update-server-info 9 | -------------------------------------------------------------------------------- /spec/fixtures/irebasing.git/hooks/pre-applypatch.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to verify what is about to be committed 4 | # by applypatch from an e-mail message. 5 | # 6 | # The hook should exit with non-zero status after issuing an 7 | # appropriate message if it wants to stop the commit. 8 | # 9 | # To enable this hook, rename this file to "pre-applypatch". 10 | 11 | . git-sh-setup 12 | test -x "$GIT_DIR/hooks/pre-commit" && 13 | exec "$GIT_DIR/hooks/pre-commit" ${1+"$@"} 14 | : 15 | -------------------------------------------------------------------------------- /spec/fixtures/irebasing.git/hooks/pre-commit.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to verify what is about to be committed. 4 | # Called by "git commit" with no arguments. The hook should 5 | # exit with non-zero status after issuing an appropriate message if 6 | # it wants to stop the commit. 7 | # 8 | # To enable this hook, rename this file to "pre-commit". 9 | 10 | if git rev-parse --verify HEAD >/dev/null 2>&1 11 | then 12 | against=HEAD 13 | else 14 | # Initial commit: diff against an empty tree object 15 | against=4b825dc642cb6eb9a060e54bf8d69288fbee4904 16 | fi 17 | 18 | # If you want to allow non-ascii filenames set this variable to true. 19 | allownonascii=$(git config hooks.allownonascii) 20 | 21 | # Redirect output to stderr. 22 | exec 1>&2 23 | 24 | # Cross platform projects tend to avoid non-ascii filenames; prevent 25 | # them from being added to the repository. We exploit the fact that the 26 | # printable range starts at the space character and ends with tilde. 27 | if [ "$allownonascii" != "true" ] && 28 | # Note that the use of brackets around a tr range is ok here, (it's 29 | # even required, for portability to Solaris 10's /usr/bin/tr), since 30 | # the square bracket bytes happen to fall in the designated range. 31 | test $(git diff --cached --name-only --diff-filter=A -z $against | 32 | LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 33 | then 34 | echo "Error: Attempt to add a non-ascii file name." 35 | echo 36 | echo "This can cause problems if you want to work" 37 | echo "with people on other platforms." 38 | echo 39 | echo "To be portable it is advisable to rename the file ..." 40 | echo 41 | echo "If you know what you are doing you can disable this" 42 | echo "check using:" 43 | echo 44 | echo " git config hooks.allownonascii true" 45 | echo 46 | exit 1 47 | fi 48 | 49 | # If there are whitespace errors, print the offending file names and fail. 50 | exec git diff-index --check --cached $against -- 51 | -------------------------------------------------------------------------------- /spec/fixtures/irebasing.git/hooks/pre-push.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # An example hook script to verify what is about to be pushed. Called by "git 4 | # push" after it has checked the remote status, but before anything has been 5 | # pushed. If this script exits with a non-zero status nothing will be pushed. 6 | # 7 | # This hook is called with the following parameters: 8 | # 9 | # $1 -- Name of the remote to which the push is being done 10 | # $2 -- URL to which the push is being done 11 | # 12 | # If pushing without using a named remote those arguments will be equal. 13 | # 14 | # Information about the commits which are being pushed is supplied as lines to 15 | # the standard input in the form: 16 | # 17 | # 18 | # 19 | # This sample shows how to prevent push of commits where the log message starts 20 | # with "WIP" (work in progress). 21 | 22 | remote="$1" 23 | url="$2" 24 | 25 | z40=0000000000000000000000000000000000000000 26 | 27 | IFS=' ' 28 | while read local_ref local_sha remote_ref remote_sha 29 | do 30 | if [ "$local_sha" = $z40 ] 31 | then 32 | # Handle delete 33 | else 34 | if [ "$remote_sha" = $z40 ] 35 | then 36 | # New branch, examine all commits 37 | range="$local_sha" 38 | else 39 | # Update to existing branch, examine new commits 40 | range="$remote_sha..$local_sha" 41 | fi 42 | 43 | # Check for WIP commit 44 | commit=`git rev-list -n 1 --grep '^WIP' "$range"` 45 | if [ -n "$commit" ] 46 | then 47 | echo "Found WIP commit in $local_ref, not pushing" 48 | exit 1 49 | fi 50 | fi 51 | done 52 | 53 | exit 0 54 | -------------------------------------------------------------------------------- /spec/fixtures/irebasing.git/hooks/pre-rebase.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Copyright (c) 2006, 2008 Junio C Hamano 4 | # 5 | # The "pre-rebase" hook is run just before "git rebase" starts doing 6 | # its job, and can prevent the command from running by exiting with 7 | # non-zero status. 8 | # 9 | # The hook is called with the following parameters: 10 | # 11 | # $1 -- the upstream the series was forked from. 12 | # $2 -- the branch being rebased (or empty when rebasing the current branch). 13 | # 14 | # This sample shows how to prevent topic branches that are already 15 | # merged to 'next' branch from getting rebased, because allowing it 16 | # would result in rebasing already published history. 17 | 18 | publish=next 19 | basebranch="$1" 20 | if test "$#" = 2 21 | then 22 | topic="refs/heads/$2" 23 | else 24 | topic=`git symbolic-ref HEAD` || 25 | exit 0 ;# we do not interrupt rebasing detached HEAD 26 | fi 27 | 28 | case "$topic" in 29 | refs/heads/??/*) 30 | ;; 31 | *) 32 | exit 0 ;# we do not interrupt others. 33 | ;; 34 | esac 35 | 36 | # Now we are dealing with a topic branch being rebased 37 | # on top of master. Is it OK to rebase it? 38 | 39 | # Does the topic really exist? 40 | git show-ref -q "$topic" || { 41 | echo >&2 "No such branch $topic" 42 | exit 1 43 | } 44 | 45 | # Is topic fully merged to master? 46 | not_in_master=`git rev-list --pretty=oneline ^master "$topic"` 47 | if test -z "$not_in_master" 48 | then 49 | echo >&2 "$topic is fully merged to master; better remove it." 50 | exit 1 ;# we could allow it, but there is no point. 51 | fi 52 | 53 | # Is topic ever merged to next? If so you should not be rebasing it. 54 | only_next_1=`git rev-list ^master "^$topic" ${publish} | sort` 55 | only_next_2=`git rev-list ^master ${publish} | sort` 56 | if test "$only_next_1" = "$only_next_2" 57 | then 58 | not_in_topic=`git rev-list "^$topic" master` 59 | if test -z "$not_in_topic" 60 | then 61 | echo >&2 "$topic is already up-to-date with master" 62 | exit 1 ;# we could allow it, but there is no point. 63 | else 64 | exit 0 65 | fi 66 | else 67 | not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"` 68 | /usr/bin/perl -e ' 69 | my $topic = $ARGV[0]; 70 | my $msg = "* $topic has commits already merged to public branch:\n"; 71 | my (%not_in_next) = map { 72 | /^([0-9a-f]+) /; 73 | ($1 => 1); 74 | } split(/\n/, $ARGV[1]); 75 | for my $elem (map { 76 | /^([0-9a-f]+) (.*)$/; 77 | [$1 => $2]; 78 | } split(/\n/, $ARGV[2])) { 79 | if (!exists $not_in_next{$elem->[0]}) { 80 | if ($msg) { 81 | print STDERR $msg; 82 | undef $msg; 83 | } 84 | print STDERR " $elem->[1]\n"; 85 | } 86 | } 87 | ' "$topic" "$not_in_next" "$not_in_master" 88 | exit 1 89 | fi 90 | 91 | exit 0 92 | 93 | ################################################################ 94 | 95 | This sample hook safeguards topic branches that have been 96 | published from being rewound. 97 | 98 | The workflow assumed here is: 99 | 100 | * Once a topic branch forks from "master", "master" is never 101 | merged into it again (either directly or indirectly). 102 | 103 | * Once a topic branch is fully cooked and merged into "master", 104 | it is deleted. If you need to build on top of it to correct 105 | earlier mistakes, a new topic branch is created by forking at 106 | the tip of the "master". This is not strictly necessary, but 107 | it makes it easier to keep your history simple. 108 | 109 | * Whenever you need to test or publish your changes to topic 110 | branches, merge them into "next" branch. 111 | 112 | The script, being an example, hardcodes the publish branch name 113 | to be "next", but it is trivial to make it configurable via 114 | $GIT_DIR/config mechanism. 115 | 116 | With this workflow, you would want to know: 117 | 118 | (1) ... if a topic branch has ever been merged to "next". Young 119 | topic branches can have stupid mistakes you would rather 120 | clean up before publishing, and things that have not been 121 | merged into other branches can be easily rebased without 122 | affecting other people. But once it is published, you would 123 | not want to rewind it. 124 | 125 | (2) ... if a topic branch has been fully merged to "master". 126 | Then you can delete it. More importantly, you should not 127 | build on top of it -- other people may already want to 128 | change things related to the topic as patches against your 129 | "master", so if you need further changes, it is better to 130 | fork the topic (perhaps with the same name) afresh from the 131 | tip of "master". 132 | 133 | Let's look at this example: 134 | 135 | o---o---o---o---o---o---o---o---o---o "next" 136 | / / / / 137 | / a---a---b A / / 138 | / / / / 139 | / / c---c---c---c B / 140 | / / / \ / 141 | / / / b---b C \ / 142 | / / / / \ / 143 | ---o---o---o---o---o---o---o---o---o---o---o "master" 144 | 145 | 146 | A, B and C are topic branches. 147 | 148 | * A has one fix since it was merged up to "next". 149 | 150 | * B has finished. It has been fully merged up to "master" and "next", 151 | and is ready to be deleted. 152 | 153 | * C has not merged to "next" at all. 154 | 155 | We would want to allow C to be rebased, refuse A, and encourage 156 | B to be deleted. 157 | 158 | To compute (1): 159 | 160 | git rev-list ^master ^topic next 161 | git rev-list ^master next 162 | 163 | if these match, topic has not merged in next at all. 164 | 165 | To compute (2): 166 | 167 | git rev-list master..topic 168 | 169 | if this is empty, it is fully merged to "master". 170 | -------------------------------------------------------------------------------- /spec/fixtures/irebasing.git/hooks/prepare-commit-msg.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to prepare the commit log message. 4 | # Called by "git commit" with the name of the file that has the 5 | # commit message, followed by the description of the commit 6 | # message's source. The hook's purpose is to edit the commit 7 | # message file. If the hook fails with a non-zero status, 8 | # the commit is aborted. 9 | # 10 | # To enable this hook, rename this file to "prepare-commit-msg". 11 | 12 | # This hook includes three examples. The first comments out the 13 | # "Conflicts:" part of a merge commit. 14 | # 15 | # The second includes the output of "git diff --name-status -r" 16 | # into the message, just before the "git status" output. It is 17 | # commented because it doesn't cope with --amend or with squashed 18 | # commits. 19 | # 20 | # The third example adds a Signed-off-by line to the message, that can 21 | # still be edited. This is rarely a good idea. 22 | 23 | case "$2,$3" in 24 | merge,) 25 | /usr/bin/perl -i.bak -ne 's/^/# /, s/^# #/#/ if /^Conflicts/ .. /#/; print' "$1" ;; 26 | 27 | # ,|template,) 28 | # /usr/bin/perl -i.bak -pe ' 29 | # print "\n" . `git diff --cached --name-status -r` 30 | # if /^#/ && $first++ == 0' "$1" ;; 31 | 32 | *) ;; 33 | esac 34 | 35 | # SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') 36 | # grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" 37 | -------------------------------------------------------------------------------- /spec/fixtures/irebasing.git/hooks/update.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to blocks unannotated tags from entering. 4 | # Called by "git receive-pack" with arguments: refname sha1-old sha1-new 5 | # 6 | # To enable this hook, rename this file to "update". 7 | # 8 | # Config 9 | # ------ 10 | # hooks.allowunannotated 11 | # This boolean sets whether unannotated tags will be allowed into the 12 | # repository. By default they won't be. 13 | # hooks.allowdeletetag 14 | # This boolean sets whether deleting tags will be allowed in the 15 | # repository. By default they won't be. 16 | # hooks.allowmodifytag 17 | # This boolean sets whether a tag may be modified after creation. By default 18 | # it won't be. 19 | # hooks.allowdeletebranch 20 | # This boolean sets whether deleting branches will be allowed in the 21 | # repository. By default they won't be. 22 | # hooks.denycreatebranch 23 | # This boolean sets whether remotely creating branches will be denied 24 | # in the repository. By default this is allowed. 25 | # 26 | 27 | # --- Command line 28 | refname="$1" 29 | oldrev="$2" 30 | newrev="$3" 31 | 32 | # --- Safety check 33 | if [ -z "$GIT_DIR" ]; then 34 | echo "Don't run this script from the command line." >&2 35 | echo " (if you want, you could supply GIT_DIR then run" >&2 36 | echo " $0 )" >&2 37 | exit 1 38 | fi 39 | 40 | if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then 41 | echo "usage: $0 " >&2 42 | exit 1 43 | fi 44 | 45 | # --- Config 46 | allowunannotated=$(git config --bool hooks.allowunannotated) 47 | allowdeletebranch=$(git config --bool hooks.allowdeletebranch) 48 | denycreatebranch=$(git config --bool hooks.denycreatebranch) 49 | allowdeletetag=$(git config --bool hooks.allowdeletetag) 50 | allowmodifytag=$(git config --bool hooks.allowmodifytag) 51 | 52 | # check for no description 53 | projectdesc=$(sed -e '1q' "$GIT_DIR/description") 54 | case "$projectdesc" in 55 | "Unnamed repository"* | "") 56 | echo "*** Project description file hasn't been set" >&2 57 | exit 1 58 | ;; 59 | esac 60 | 61 | # --- Check types 62 | # if $newrev is 0000...0000, it's a commit to delete a ref. 63 | zero="0000000000000000000000000000000000000000" 64 | if [ "$newrev" = "$zero" ]; then 65 | newrev_type=delete 66 | else 67 | newrev_type=$(git cat-file -t $newrev) 68 | fi 69 | 70 | case "$refname","$newrev_type" in 71 | refs/tags/*,commit) 72 | # un-annotated tag 73 | short_refname=${refname##refs/tags/} 74 | if [ "$allowunannotated" != "true" ]; then 75 | echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2 76 | echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2 77 | exit 1 78 | fi 79 | ;; 80 | refs/tags/*,delete) 81 | # delete tag 82 | if [ "$allowdeletetag" != "true" ]; then 83 | echo "*** Deleting a tag is not allowed in this repository" >&2 84 | exit 1 85 | fi 86 | ;; 87 | refs/tags/*,tag) 88 | # annotated tag 89 | if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1 90 | then 91 | echo "*** Tag '$refname' already exists." >&2 92 | echo "*** Modifying a tag is not allowed in this repository." >&2 93 | exit 1 94 | fi 95 | ;; 96 | refs/heads/*,commit) 97 | # branch 98 | if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then 99 | echo "*** Creating a branch is not allowed in this repository" >&2 100 | exit 1 101 | fi 102 | ;; 103 | refs/heads/*,delete) 104 | # delete branch 105 | if [ "$allowdeletebranch" != "true" ]; then 106 | echo "*** Deleting a branch is not allowed in this repository" >&2 107 | exit 1 108 | fi 109 | ;; 110 | refs/remotes/*,commit) 111 | # tracking branch 112 | ;; 113 | refs/remotes/*,delete) 114 | # delete tracking branch 115 | if [ "$allowdeletebranch" != "true" ]; then 116 | echo "*** Deleting a tracking branch is not allowed in this repository" >&2 117 | exit 1 118 | fi 119 | ;; 120 | *) 121 | # Anything else (is there anything else?) 122 | echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2 123 | exit 1 124 | ;; 125 | esac 126 | 127 | # --- Finished 128 | exit 0 129 | -------------------------------------------------------------------------------- /spec/fixtures/irebasing.git/index: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smashwilson/merge-conflicts/0ce713e94cb8c08ccccf015d63c2653a589737e2/spec/fixtures/irebasing.git/index -------------------------------------------------------------------------------- /spec/fixtures/irebasing.git/info/exclude: -------------------------------------------------------------------------------- 1 | # git ls-files --others --exclude-from=.git/info/exclude 2 | # Lines that start with '#' are comments. 3 | # For a project mostly in C, the following would be a good set of 4 | # exclude patterns (uncomment them if you want to use them): 5 | # *.[oa] 6 | # *~ 7 | -------------------------------------------------------------------------------- /spec/fixtures/irebasing.git/logs/HEAD: -------------------------------------------------------------------------------- 1 | 0000000000000000000000000000000000000000 6e3541659dc7f506812b55ce4f6f9554a420464a Ash Wilson 1393692299 -0500 commit (initial): a 2 | 6e3541659dc7f506812b55ce4f6f9554a420464a 6e3541659dc7f506812b55ce4f6f9554a420464a Ash Wilson 1393692309 -0500 checkout: moving from master to branch 3 | 6e3541659dc7f506812b55ce4f6f9554a420464a 6e3541659dc7f506812b55ce4f6f9554a420464a Ash Wilson 1393692316 -0500 checkout: moving from branch to master 4 | 6e3541659dc7f506812b55ce4f6f9554a420464a 6e3541659dc7f506812b55ce4f6f9554a420464a Ash Wilson 1393692323 -0500 checkout: moving from master to branch 5 | 6e3541659dc7f506812b55ce4f6f9554a420464a 205331815ca02331b81f804b81763ce4fed3d101 Ash Wilson 1393692327 -0500 commit: aaaa 6 | 205331815ca02331b81f804b81763ce4fed3d101 6e3541659dc7f506812b55ce4f6f9554a420464a Ash Wilson 1393692335 -0500 checkout: moving from branch to master 7 | 6e3541659dc7f506812b55ce4f6f9554a420464a 30fc72947114999af153eed9dc81729de90edc95 Ash Wilson 1393692344 -0500 commit: master 8 | 30fc72947114999af153eed9dc81729de90edc95 205331815ca02331b81f804b81763ce4fed3d101 Ash Wilson 1393694601 -0500 checkout: moving from master to branch 9 | 205331815ca02331b81f804b81763ce4fed3d101 04a3e0388ccaa0722592d849e6777c4c74eeacba Ash Wilson 1393694611 -0500 commit: file1 mod 10 | 04a3e0388ccaa0722592d849e6777c4c74eeacba 30fc72947114999af153eed9dc81729de90edc95 Ash Wilson 1393694617 -0500 checkout: moving from branch to master 11 | 30fc72947114999af153eed9dc81729de90edc95 fc37b1879fff0ec01a96bf6792b973a2b1e1ee4d Ash Wilson 1393694629 -0500 commit: file2 mod 12 | fc37b1879fff0ec01a96bf6792b973a2b1e1ee4d 79b840d95746654d662782be57ffc9cdc607e458 Ash Wilson 1394307204 -0500 commit: More changes on the master side. 13 | 79b840d95746654d662782be57ffc9cdc607e458 ab0b0263337e1bac55041d95c805cdb65b8434b4 Ash Wilson 1394307232 -0500 commit: Changes in file2 as well. 14 | ab0b0263337e1bac55041d95c805cdb65b8434b4 04a3e0388ccaa0722592d849e6777c4c74eeacba Ash Wilson 1394307241 -0500 checkout: moving from master to branch 15 | 04a3e0388ccaa0722592d849e6777c4c74eeacba 6806d7673c7e5802dc49e7a4aa3a6cd683d72eb0 Ash Wilson 1394307279 -0500 commit: More changes in branch. 16 | 6806d7673c7e5802dc49e7a4aa3a6cd683d72eb0 ab0b0263337e1bac55041d95c805cdb65b8434b4 Ash Wilson 1394307286 -0500 checkout: moving from branch to master 17 | ab0b0263337e1bac55041d95c805cdb65b8434b4 51d48cc54f3f108ed59c0c4d7470a81a6cb4896a Ash Wilson 1395272112 -0400 commit: subdir 18 | 51d48cc54f3f108ed59c0c4d7470a81a6cb4896a 6806d7673c7e5802dc49e7a4aa3a6cd683d72eb0 Ash Wilson 1397570145 -0400 checkout: moving from master to branch 19 | 6806d7673c7e5802dc49e7a4aa3a6cd683d72eb0 6806d7673c7e5802dc49e7a4aa3a6cd683d72eb0 Ash Wilson 1397570157 -0400 checkout: moving from branch to rebaser 20 | 6806d7673c7e5802dc49e7a4aa3a6cd683d72eb0 51d48cc54f3f108ed59c0c4d7470a81a6cb4896a Ash Wilson 1397570168 -0400 rebase: checkout master 21 | 51d48cc54f3f108ed59c0c4d7470a81a6cb4896a 6806d7673c7e5802dc49e7a4aa3a6cd683d72eb0 Ash Wilson 1397571353 -0400 rebase: aborting 22 | 6806d7673c7e5802dc49e7a4aa3a6cd683d72eb0 51d48cc54f3f108ed59c0c4d7470a81a6cb4896a Ash Wilson 1397571363 -0400 rebase -i (start): checkout master 23 | -------------------------------------------------------------------------------- /spec/fixtures/irebasing.git/logs/refs/heads/branch: -------------------------------------------------------------------------------- 1 | 0000000000000000000000000000000000000000 6e3541659dc7f506812b55ce4f6f9554a420464a Ash Wilson 1393692309 -0500 branch: Created from HEAD 2 | 6e3541659dc7f506812b55ce4f6f9554a420464a 205331815ca02331b81f804b81763ce4fed3d101 Ash Wilson 1393692327 -0500 commit: aaaa 3 | 205331815ca02331b81f804b81763ce4fed3d101 04a3e0388ccaa0722592d849e6777c4c74eeacba Ash Wilson 1393694611 -0500 commit: file1 mod 4 | 04a3e0388ccaa0722592d849e6777c4c74eeacba 6806d7673c7e5802dc49e7a4aa3a6cd683d72eb0 Ash Wilson 1394307279 -0500 commit: More changes in branch. 5 | -------------------------------------------------------------------------------- /spec/fixtures/irebasing.git/logs/refs/heads/master: -------------------------------------------------------------------------------- 1 | 0000000000000000000000000000000000000000 6e3541659dc7f506812b55ce4f6f9554a420464a Ash Wilson 1393692299 -0500 commit (initial): a 2 | 6e3541659dc7f506812b55ce4f6f9554a420464a 30fc72947114999af153eed9dc81729de90edc95 Ash Wilson 1393692344 -0500 commit: master 3 | 30fc72947114999af153eed9dc81729de90edc95 fc37b1879fff0ec01a96bf6792b973a2b1e1ee4d Ash Wilson 1393694629 -0500 commit: file2 mod 4 | fc37b1879fff0ec01a96bf6792b973a2b1e1ee4d 79b840d95746654d662782be57ffc9cdc607e458 Ash Wilson 1394307204 -0500 commit: More changes on the master side. 5 | 79b840d95746654d662782be57ffc9cdc607e458 ab0b0263337e1bac55041d95c805cdb65b8434b4 Ash Wilson 1394307232 -0500 commit: Changes in file2 as well. 6 | ab0b0263337e1bac55041d95c805cdb65b8434b4 51d48cc54f3f108ed59c0c4d7470a81a6cb4896a Ash Wilson 1395272112 -0400 commit: subdir 7 | -------------------------------------------------------------------------------- /spec/fixtures/irebasing.git/logs/refs/heads/rebaser: -------------------------------------------------------------------------------- 1 | 0000000000000000000000000000000000000000 6806d7673c7e5802dc49e7a4aa3a6cd683d72eb0 Ash Wilson 1397570151 -0400 branch: Created from branch 2 | -------------------------------------------------------------------------------- /spec/fixtures/irebasing.git/rebase-merge/author-script: -------------------------------------------------------------------------------- 1 | GIT_AUTHOR_NAME='Ash Wilson' 2 | GIT_AUTHOR_EMAIL='smashwilson@gmail.com' 3 | GIT_AUTHOR_DATE='@1393692327 -0500' 4 | -------------------------------------------------------------------------------- /spec/fixtures/irebasing.git/rebase-merge/done: -------------------------------------------------------------------------------- 1 | pick 205331815ca02331b81f804b81763ce4fed3d101 aaaa 2 | -------------------------------------------------------------------------------- /spec/fixtures/irebasing.git/rebase-merge/end: -------------------------------------------------------------------------------- 1 | 3 2 | -------------------------------------------------------------------------------- /spec/fixtures/irebasing.git/rebase-merge/git-rebase-todo: -------------------------------------------------------------------------------- 1 | pick 04a3e0388ccaa0722592d849e6777c4c74eeacba file1 mod 2 | pick 6806d7673c7e5802dc49e7a4aa3a6cd683d72eb0 More changes in branch. 3 | 4 | # Rebase 51d48cc..6806d76 onto 51d48cc 5 | # 6 | # Commands: 7 | # p, pick = use commit 8 | # r, reword = use commit, but edit the commit message 9 | # e, edit = use commit, but stop for amending 10 | # s, squash = use commit, but meld into previous commit 11 | # f, fixup = like "squash", but discard this commit's log message 12 | # x, exec = run command (the rest of the line) using shell 13 | # 14 | # These lines can be re-ordered; they are executed from top to bottom. 15 | # 16 | # If you remove a line here THAT COMMIT WILL BE LOST. 17 | # 18 | # However, if you remove everything, the rebase will be aborted. 19 | # 20 | # Note that empty commits are commented out 21 | -------------------------------------------------------------------------------- /spec/fixtures/irebasing.git/rebase-merge/git-rebase-todo.backup: -------------------------------------------------------------------------------- 1 | pick 2053318 aaaa 2 | pick 04a3e03 file1 mod 3 | pick 6806d76 More changes in branch. 4 | 5 | # Rebase 51d48cc..6806d76 onto 51d48cc 6 | # 7 | # Commands: 8 | # p, pick = use commit 9 | # r, reword = use commit, but edit the commit message 10 | # e, edit = use commit, but stop for amending 11 | # s, squash = use commit, but meld into previous commit 12 | # f, fixup = like "squash", but discard this commit's log message 13 | # x, exec = run command (the rest of the line) using shell 14 | # 15 | # These lines can be re-ordered; they are executed from top to bottom. 16 | # 17 | # If you remove a line here THAT COMMIT WILL BE LOST. 18 | # 19 | # However, if you remove everything, the rebase will be aborted. 20 | # 21 | # Note that empty commits are commented out 22 | -------------------------------------------------------------------------------- /spec/fixtures/irebasing.git/rebase-merge/head-name: -------------------------------------------------------------------------------- 1 | refs/heads/rebaser 2 | -------------------------------------------------------------------------------- /spec/fixtures/irebasing.git/rebase-merge/interactive: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smashwilson/merge-conflicts/0ce713e94cb8c08ccccf015d63c2653a589737e2/spec/fixtures/irebasing.git/rebase-merge/interactive -------------------------------------------------------------------------------- /spec/fixtures/irebasing.git/rebase-merge/message: -------------------------------------------------------------------------------- 1 | aaaa 2 | -------------------------------------------------------------------------------- /spec/fixtures/irebasing.git/rebase-merge/msgnum: -------------------------------------------------------------------------------- 1 | 1 2 | -------------------------------------------------------------------------------- /spec/fixtures/irebasing.git/rebase-merge/onto: -------------------------------------------------------------------------------- 1 | 51d48cc54f3f108ed59c0c4d7470a81a6cb4896a 2 | -------------------------------------------------------------------------------- /spec/fixtures/irebasing.git/rebase-merge/orig-head: -------------------------------------------------------------------------------- 1 | 6806d7673c7e5802dc49e7a4aa3a6cd683d72eb0 2 | -------------------------------------------------------------------------------- /spec/fixtures/irebasing.git/rebase-merge/patch: -------------------------------------------------------------------------------- 1 | diff --git a/file b/file 2 | index e69de29..ccc3e7b 100644 3 | --- a/file 4 | +++ b/file 5 | @@ -0,0 +1 @@ 6 | +aaaaa 7 | -------------------------------------------------------------------------------- /spec/fixtures/irebasing.git/rebase-merge/quiet: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /spec/fixtures/irebasing.git/rebase-merge/stopped-sha: -------------------------------------------------------------------------------- 1 | 205331815ca02331b81f804b81763ce4fed3d101 2 | -------------------------------------------------------------------------------- /spec/fixtures/irebasing.git/refs/heads/branch: -------------------------------------------------------------------------------- 1 | 6806d7673c7e5802dc49e7a4aa3a6cd683d72eb0 2 | -------------------------------------------------------------------------------- /spec/fixtures/irebasing.git/refs/heads/master: -------------------------------------------------------------------------------- 1 | 51d48cc54f3f108ed59c0c4d7470a81a6cb4896a 2 | -------------------------------------------------------------------------------- /spec/fixtures/irebasing.git/refs/heads/rebaser: -------------------------------------------------------------------------------- 1 | 6806d7673c7e5802dc49e7a4aa3a6cd683d72eb0 2 | -------------------------------------------------------------------------------- /spec/fixtures/merging.git/COMMIT_EDITMSG: -------------------------------------------------------------------------------- 1 | subdir 2 | -------------------------------------------------------------------------------- /spec/fixtures/merging.git/HEAD: -------------------------------------------------------------------------------- 1 | ref: refs/heads/rebaser 2 | -------------------------------------------------------------------------------- /spec/fixtures/merging.git/MERGE_HEAD: -------------------------------------------------------------------------------- 1 | 51d48cc54f3f108ed59c0c4d7470a81a6cb4896a 2 | -------------------------------------------------------------------------------- /spec/fixtures/merging.git/MERGE_MODE: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smashwilson/merge-conflicts/0ce713e94cb8c08ccccf015d63c2653a589737e2/spec/fixtures/merging.git/MERGE_MODE -------------------------------------------------------------------------------- /spec/fixtures/merging.git/MERGE_MSG: -------------------------------------------------------------------------------- 1 | Merge branch 'master' into rebaser 2 | 3 | Conflicts: 4 | file 5 | file2 6 | -------------------------------------------------------------------------------- /spec/fixtures/merging.git/ORIG_HEAD: -------------------------------------------------------------------------------- 1 | 6806d7673c7e5802dc49e7a4aa3a6cd683d72eb0 2 | -------------------------------------------------------------------------------- /spec/fixtures/merging.git/config: -------------------------------------------------------------------------------- 1 | [core] 2 | repositoryformatversion = 0 3 | filemode = true 4 | bare = false 5 | logallrefupdates = true 6 | ignorecase = true 7 | precomposeunicode = false 8 | -------------------------------------------------------------------------------- /spec/fixtures/merging.git/description: -------------------------------------------------------------------------------- 1 | Unnamed repository; edit this file 'description' to name the repository. 2 | -------------------------------------------------------------------------------- /spec/fixtures/merging.git/hooks/applypatch-msg.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to check the commit log message taken by 4 | # applypatch from an e-mail message. 5 | # 6 | # The hook should exit with non-zero status after issuing an 7 | # appropriate message if it wants to stop the commit. The hook is 8 | # allowed to edit the commit message file. 9 | # 10 | # To enable this hook, rename this file to "applypatch-msg". 11 | 12 | . git-sh-setup 13 | test -x "$GIT_DIR/hooks/commit-msg" && 14 | exec "$GIT_DIR/hooks/commit-msg" ${1+"$@"} 15 | : 16 | -------------------------------------------------------------------------------- /spec/fixtures/merging.git/hooks/commit-msg.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to check the commit log message. 4 | # Called by "git commit" with one argument, the name of the file 5 | # that has the commit message. The hook should exit with non-zero 6 | # status after issuing an appropriate message if it wants to stop the 7 | # commit. The hook is allowed to edit the commit message file. 8 | # 9 | # To enable this hook, rename this file to "commit-msg". 10 | 11 | # Uncomment the below to add a Signed-off-by line to the message. 12 | # Doing this in a hook is a bad idea in general, but the prepare-commit-msg 13 | # hook is more suited to it. 14 | # 15 | # SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') 16 | # grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" 17 | 18 | # This example catches duplicate Signed-off-by lines. 19 | 20 | test "" = "$(grep '^Signed-off-by: ' "$1" | 21 | sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || { 22 | echo >&2 Duplicate Signed-off-by lines. 23 | exit 1 24 | } 25 | -------------------------------------------------------------------------------- /spec/fixtures/merging.git/hooks/post-update.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to prepare a packed repository for use over 4 | # dumb transports. 5 | # 6 | # To enable this hook, rename this file to "post-update". 7 | 8 | exec git update-server-info 9 | -------------------------------------------------------------------------------- /spec/fixtures/merging.git/hooks/pre-applypatch.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to verify what is about to be committed 4 | # by applypatch from an e-mail message. 5 | # 6 | # The hook should exit with non-zero status after issuing an 7 | # appropriate message if it wants to stop the commit. 8 | # 9 | # To enable this hook, rename this file to "pre-applypatch". 10 | 11 | . git-sh-setup 12 | test -x "$GIT_DIR/hooks/pre-commit" && 13 | exec "$GIT_DIR/hooks/pre-commit" ${1+"$@"} 14 | : 15 | -------------------------------------------------------------------------------- /spec/fixtures/merging.git/hooks/pre-commit.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to verify what is about to be committed. 4 | # Called by "git commit" with no arguments. The hook should 5 | # exit with non-zero status after issuing an appropriate message if 6 | # it wants to stop the commit. 7 | # 8 | # To enable this hook, rename this file to "pre-commit". 9 | 10 | if git rev-parse --verify HEAD >/dev/null 2>&1 11 | then 12 | against=HEAD 13 | else 14 | # Initial commit: diff against an empty tree object 15 | against=4b825dc642cb6eb9a060e54bf8d69288fbee4904 16 | fi 17 | 18 | # If you want to allow non-ascii filenames set this variable to true. 19 | allownonascii=$(git config hooks.allownonascii) 20 | 21 | # Redirect output to stderr. 22 | exec 1>&2 23 | 24 | # Cross platform projects tend to avoid non-ascii filenames; prevent 25 | # them from being added to the repository. We exploit the fact that the 26 | # printable range starts at the space character and ends with tilde. 27 | if [ "$allownonascii" != "true" ] && 28 | # Note that the use of brackets around a tr range is ok here, (it's 29 | # even required, for portability to Solaris 10's /usr/bin/tr), since 30 | # the square bracket bytes happen to fall in the designated range. 31 | test $(git diff --cached --name-only --diff-filter=A -z $against | 32 | LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 33 | then 34 | echo "Error: Attempt to add a non-ascii file name." 35 | echo 36 | echo "This can cause problems if you want to work" 37 | echo "with people on other platforms." 38 | echo 39 | echo "To be portable it is advisable to rename the file ..." 40 | echo 41 | echo "If you know what you are doing you can disable this" 42 | echo "check using:" 43 | echo 44 | echo " git config hooks.allownonascii true" 45 | echo 46 | exit 1 47 | fi 48 | 49 | # If there are whitespace errors, print the offending file names and fail. 50 | exec git diff-index --check --cached $against -- 51 | -------------------------------------------------------------------------------- /spec/fixtures/merging.git/hooks/pre-push.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # An example hook script to verify what is about to be pushed. Called by "git 4 | # push" after it has checked the remote status, but before anything has been 5 | # pushed. If this script exits with a non-zero status nothing will be pushed. 6 | # 7 | # This hook is called with the following parameters: 8 | # 9 | # $1 -- Name of the remote to which the push is being done 10 | # $2 -- URL to which the push is being done 11 | # 12 | # If pushing without using a named remote those arguments will be equal. 13 | # 14 | # Information about the commits which are being pushed is supplied as lines to 15 | # the standard input in the form: 16 | # 17 | # 18 | # 19 | # This sample shows how to prevent push of commits where the log message starts 20 | # with "WIP" (work in progress). 21 | 22 | remote="$1" 23 | url="$2" 24 | 25 | z40=0000000000000000000000000000000000000000 26 | 27 | IFS=' ' 28 | while read local_ref local_sha remote_ref remote_sha 29 | do 30 | if [ "$local_sha" = $z40 ] 31 | then 32 | # Handle delete 33 | else 34 | if [ "$remote_sha" = $z40 ] 35 | then 36 | # New branch, examine all commits 37 | range="$local_sha" 38 | else 39 | # Update to existing branch, examine new commits 40 | range="$remote_sha..$local_sha" 41 | fi 42 | 43 | # Check for WIP commit 44 | commit=`git rev-list -n 1 --grep '^WIP' "$range"` 45 | if [ -n "$commit" ] 46 | then 47 | echo "Found WIP commit in $local_ref, not pushing" 48 | exit 1 49 | fi 50 | fi 51 | done 52 | 53 | exit 0 54 | -------------------------------------------------------------------------------- /spec/fixtures/merging.git/hooks/pre-rebase.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Copyright (c) 2006, 2008 Junio C Hamano 4 | # 5 | # The "pre-rebase" hook is run just before "git rebase" starts doing 6 | # its job, and can prevent the command from running by exiting with 7 | # non-zero status. 8 | # 9 | # The hook is called with the following parameters: 10 | # 11 | # $1 -- the upstream the series was forked from. 12 | # $2 -- the branch being rebased (or empty when rebasing the current branch). 13 | # 14 | # This sample shows how to prevent topic branches that are already 15 | # merged to 'next' branch from getting rebased, because allowing it 16 | # would result in rebasing already published history. 17 | 18 | publish=next 19 | basebranch="$1" 20 | if test "$#" = 2 21 | then 22 | topic="refs/heads/$2" 23 | else 24 | topic=`git symbolic-ref HEAD` || 25 | exit 0 ;# we do not interrupt rebasing detached HEAD 26 | fi 27 | 28 | case "$topic" in 29 | refs/heads/??/*) 30 | ;; 31 | *) 32 | exit 0 ;# we do not interrupt others. 33 | ;; 34 | esac 35 | 36 | # Now we are dealing with a topic branch being rebased 37 | # on top of master. Is it OK to rebase it? 38 | 39 | # Does the topic really exist? 40 | git show-ref -q "$topic" || { 41 | echo >&2 "No such branch $topic" 42 | exit 1 43 | } 44 | 45 | # Is topic fully merged to master? 46 | not_in_master=`git rev-list --pretty=oneline ^master "$topic"` 47 | if test -z "$not_in_master" 48 | then 49 | echo >&2 "$topic is fully merged to master; better remove it." 50 | exit 1 ;# we could allow it, but there is no point. 51 | fi 52 | 53 | # Is topic ever merged to next? If so you should not be rebasing it. 54 | only_next_1=`git rev-list ^master "^$topic" ${publish} | sort` 55 | only_next_2=`git rev-list ^master ${publish} | sort` 56 | if test "$only_next_1" = "$only_next_2" 57 | then 58 | not_in_topic=`git rev-list "^$topic" master` 59 | if test -z "$not_in_topic" 60 | then 61 | echo >&2 "$topic is already up-to-date with master" 62 | exit 1 ;# we could allow it, but there is no point. 63 | else 64 | exit 0 65 | fi 66 | else 67 | not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"` 68 | /usr/bin/perl -e ' 69 | my $topic = $ARGV[0]; 70 | my $msg = "* $topic has commits already merged to public branch:\n"; 71 | my (%not_in_next) = map { 72 | /^([0-9a-f]+) /; 73 | ($1 => 1); 74 | } split(/\n/, $ARGV[1]); 75 | for my $elem (map { 76 | /^([0-9a-f]+) (.*)$/; 77 | [$1 => $2]; 78 | } split(/\n/, $ARGV[2])) { 79 | if (!exists $not_in_next{$elem->[0]}) { 80 | if ($msg) { 81 | print STDERR $msg; 82 | undef $msg; 83 | } 84 | print STDERR " $elem->[1]\n"; 85 | } 86 | } 87 | ' "$topic" "$not_in_next" "$not_in_master" 88 | exit 1 89 | fi 90 | 91 | exit 0 92 | 93 | ################################################################ 94 | 95 | This sample hook safeguards topic branches that have been 96 | published from being rewound. 97 | 98 | The workflow assumed here is: 99 | 100 | * Once a topic branch forks from "master", "master" is never 101 | merged into it again (either directly or indirectly). 102 | 103 | * Once a topic branch is fully cooked and merged into "master", 104 | it is deleted. If you need to build on top of it to correct 105 | earlier mistakes, a new topic branch is created by forking at 106 | the tip of the "master". This is not strictly necessary, but 107 | it makes it easier to keep your history simple. 108 | 109 | * Whenever you need to test or publish your changes to topic 110 | branches, merge them into "next" branch. 111 | 112 | The script, being an example, hardcodes the publish branch name 113 | to be "next", but it is trivial to make it configurable via 114 | $GIT_DIR/config mechanism. 115 | 116 | With this workflow, you would want to know: 117 | 118 | (1) ... if a topic branch has ever been merged to "next". Young 119 | topic branches can have stupid mistakes you would rather 120 | clean up before publishing, and things that have not been 121 | merged into other branches can be easily rebased without 122 | affecting other people. But once it is published, you would 123 | not want to rewind it. 124 | 125 | (2) ... if a topic branch has been fully merged to "master". 126 | Then you can delete it. More importantly, you should not 127 | build on top of it -- other people may already want to 128 | change things related to the topic as patches against your 129 | "master", so if you need further changes, it is better to 130 | fork the topic (perhaps with the same name) afresh from the 131 | tip of "master". 132 | 133 | Let's look at this example: 134 | 135 | o---o---o---o---o---o---o---o---o---o "next" 136 | / / / / 137 | / a---a---b A / / 138 | / / / / 139 | / / c---c---c---c B / 140 | / / / \ / 141 | / / / b---b C \ / 142 | / / / / \ / 143 | ---o---o---o---o---o---o---o---o---o---o---o "master" 144 | 145 | 146 | A, B and C are topic branches. 147 | 148 | * A has one fix since it was merged up to "next". 149 | 150 | * B has finished. It has been fully merged up to "master" and "next", 151 | and is ready to be deleted. 152 | 153 | * C has not merged to "next" at all. 154 | 155 | We would want to allow C to be rebased, refuse A, and encourage 156 | B to be deleted. 157 | 158 | To compute (1): 159 | 160 | git rev-list ^master ^topic next 161 | git rev-list ^master next 162 | 163 | if these match, topic has not merged in next at all. 164 | 165 | To compute (2): 166 | 167 | git rev-list master..topic 168 | 169 | if this is empty, it is fully merged to "master". 170 | -------------------------------------------------------------------------------- /spec/fixtures/merging.git/hooks/prepare-commit-msg.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to prepare the commit log message. 4 | # Called by "git commit" with the name of the file that has the 5 | # commit message, followed by the description of the commit 6 | # message's source. The hook's purpose is to edit the commit 7 | # message file. If the hook fails with a non-zero status, 8 | # the commit is aborted. 9 | # 10 | # To enable this hook, rename this file to "prepare-commit-msg". 11 | 12 | # This hook includes three examples. The first comments out the 13 | # "Conflicts:" part of a merge commit. 14 | # 15 | # The second includes the output of "git diff --name-status -r" 16 | # into the message, just before the "git status" output. It is 17 | # commented because it doesn't cope with --amend or with squashed 18 | # commits. 19 | # 20 | # The third example adds a Signed-off-by line to the message, that can 21 | # still be edited. This is rarely a good idea. 22 | 23 | case "$2,$3" in 24 | merge,) 25 | /usr/bin/perl -i.bak -ne 's/^/# /, s/^# #/#/ if /^Conflicts/ .. /#/; print' "$1" ;; 26 | 27 | # ,|template,) 28 | # /usr/bin/perl -i.bak -pe ' 29 | # print "\n" . `git diff --cached --name-status -r` 30 | # if /^#/ && $first++ == 0' "$1" ;; 31 | 32 | *) ;; 33 | esac 34 | 35 | # SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') 36 | # grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" 37 | -------------------------------------------------------------------------------- /spec/fixtures/merging.git/hooks/update.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to blocks unannotated tags from entering. 4 | # Called by "git receive-pack" with arguments: refname sha1-old sha1-new 5 | # 6 | # To enable this hook, rename this file to "update". 7 | # 8 | # Config 9 | # ------ 10 | # hooks.allowunannotated 11 | # This boolean sets whether unannotated tags will be allowed into the 12 | # repository. By default they won't be. 13 | # hooks.allowdeletetag 14 | # This boolean sets whether deleting tags will be allowed in the 15 | # repository. By default they won't be. 16 | # hooks.allowmodifytag 17 | # This boolean sets whether a tag may be modified after creation. By default 18 | # it won't be. 19 | # hooks.allowdeletebranch 20 | # This boolean sets whether deleting branches will be allowed in the 21 | # repository. By default they won't be. 22 | # hooks.denycreatebranch 23 | # This boolean sets whether remotely creating branches will be denied 24 | # in the repository. By default this is allowed. 25 | # 26 | 27 | # --- Command line 28 | refname="$1" 29 | oldrev="$2" 30 | newrev="$3" 31 | 32 | # --- Safety check 33 | if [ -z "$GIT_DIR" ]; then 34 | echo "Don't run this script from the command line." >&2 35 | echo " (if you want, you could supply GIT_DIR then run" >&2 36 | echo " $0 )" >&2 37 | exit 1 38 | fi 39 | 40 | if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then 41 | echo "usage: $0 " >&2 42 | exit 1 43 | fi 44 | 45 | # --- Config 46 | allowunannotated=$(git config --bool hooks.allowunannotated) 47 | allowdeletebranch=$(git config --bool hooks.allowdeletebranch) 48 | denycreatebranch=$(git config --bool hooks.denycreatebranch) 49 | allowdeletetag=$(git config --bool hooks.allowdeletetag) 50 | allowmodifytag=$(git config --bool hooks.allowmodifytag) 51 | 52 | # check for no description 53 | projectdesc=$(sed -e '1q' "$GIT_DIR/description") 54 | case "$projectdesc" in 55 | "Unnamed repository"* | "") 56 | echo "*** Project description file hasn't been set" >&2 57 | exit 1 58 | ;; 59 | esac 60 | 61 | # --- Check types 62 | # if $newrev is 0000...0000, it's a commit to delete a ref. 63 | zero="0000000000000000000000000000000000000000" 64 | if [ "$newrev" = "$zero" ]; then 65 | newrev_type=delete 66 | else 67 | newrev_type=$(git cat-file -t $newrev) 68 | fi 69 | 70 | case "$refname","$newrev_type" in 71 | refs/tags/*,commit) 72 | # un-annotated tag 73 | short_refname=${refname##refs/tags/} 74 | if [ "$allowunannotated" != "true" ]; then 75 | echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2 76 | echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2 77 | exit 1 78 | fi 79 | ;; 80 | refs/tags/*,delete) 81 | # delete tag 82 | if [ "$allowdeletetag" != "true" ]; then 83 | echo "*** Deleting a tag is not allowed in this repository" >&2 84 | exit 1 85 | fi 86 | ;; 87 | refs/tags/*,tag) 88 | # annotated tag 89 | if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1 90 | then 91 | echo "*** Tag '$refname' already exists." >&2 92 | echo "*** Modifying a tag is not allowed in this repository." >&2 93 | exit 1 94 | fi 95 | ;; 96 | refs/heads/*,commit) 97 | # branch 98 | if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then 99 | echo "*** Creating a branch is not allowed in this repository" >&2 100 | exit 1 101 | fi 102 | ;; 103 | refs/heads/*,delete) 104 | # delete branch 105 | if [ "$allowdeletebranch" != "true" ]; then 106 | echo "*** Deleting a branch is not allowed in this repository" >&2 107 | exit 1 108 | fi 109 | ;; 110 | refs/remotes/*,commit) 111 | # tracking branch 112 | ;; 113 | refs/remotes/*,delete) 114 | # delete tracking branch 115 | if [ "$allowdeletebranch" != "true" ]; then 116 | echo "*** Deleting a tracking branch is not allowed in this repository" >&2 117 | exit 1 118 | fi 119 | ;; 120 | *) 121 | # Anything else (is there anything else?) 122 | echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2 123 | exit 1 124 | ;; 125 | esac 126 | 127 | # --- Finished 128 | exit 0 129 | -------------------------------------------------------------------------------- /spec/fixtures/merging.git/index: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smashwilson/merge-conflicts/0ce713e94cb8c08ccccf015d63c2653a589737e2/spec/fixtures/merging.git/index -------------------------------------------------------------------------------- /spec/fixtures/merging.git/info/exclude: -------------------------------------------------------------------------------- 1 | # git ls-files --others --exclude-from=.git/info/exclude 2 | # Lines that start with '#' are comments. 3 | # For a project mostly in C, the following would be a good set of 4 | # exclude patterns (uncomment them if you want to use them): 5 | # *.[oa] 6 | # *~ 7 | -------------------------------------------------------------------------------- /spec/fixtures/merging.git/logs/HEAD: -------------------------------------------------------------------------------- 1 | 0000000000000000000000000000000000000000 6e3541659dc7f506812b55ce4f6f9554a420464a Ash Wilson 1393692299 -0500 commit (initial): a 2 | 6e3541659dc7f506812b55ce4f6f9554a420464a 6e3541659dc7f506812b55ce4f6f9554a420464a Ash Wilson 1393692309 -0500 checkout: moving from master to branch 3 | 6e3541659dc7f506812b55ce4f6f9554a420464a 6e3541659dc7f506812b55ce4f6f9554a420464a Ash Wilson 1393692316 -0500 checkout: moving from branch to master 4 | 6e3541659dc7f506812b55ce4f6f9554a420464a 6e3541659dc7f506812b55ce4f6f9554a420464a Ash Wilson 1393692323 -0500 checkout: moving from master to branch 5 | 6e3541659dc7f506812b55ce4f6f9554a420464a 205331815ca02331b81f804b81763ce4fed3d101 Ash Wilson 1393692327 -0500 commit: aaaa 6 | 205331815ca02331b81f804b81763ce4fed3d101 6e3541659dc7f506812b55ce4f6f9554a420464a Ash Wilson 1393692335 -0500 checkout: moving from branch to master 7 | 6e3541659dc7f506812b55ce4f6f9554a420464a 30fc72947114999af153eed9dc81729de90edc95 Ash Wilson 1393692344 -0500 commit: master 8 | 30fc72947114999af153eed9dc81729de90edc95 205331815ca02331b81f804b81763ce4fed3d101 Ash Wilson 1393694601 -0500 checkout: moving from master to branch 9 | 205331815ca02331b81f804b81763ce4fed3d101 04a3e0388ccaa0722592d849e6777c4c74eeacba Ash Wilson 1393694611 -0500 commit: file1 mod 10 | 04a3e0388ccaa0722592d849e6777c4c74eeacba 30fc72947114999af153eed9dc81729de90edc95 Ash Wilson 1393694617 -0500 checkout: moving from branch to master 11 | 30fc72947114999af153eed9dc81729de90edc95 fc37b1879fff0ec01a96bf6792b973a2b1e1ee4d Ash Wilson 1393694629 -0500 commit: file2 mod 12 | fc37b1879fff0ec01a96bf6792b973a2b1e1ee4d 79b840d95746654d662782be57ffc9cdc607e458 Ash Wilson 1394307204 -0500 commit: More changes on the master side. 13 | 79b840d95746654d662782be57ffc9cdc607e458 ab0b0263337e1bac55041d95c805cdb65b8434b4 Ash Wilson 1394307232 -0500 commit: Changes in file2 as well. 14 | ab0b0263337e1bac55041d95c805cdb65b8434b4 04a3e0388ccaa0722592d849e6777c4c74eeacba Ash Wilson 1394307241 -0500 checkout: moving from master to branch 15 | 04a3e0388ccaa0722592d849e6777c4c74eeacba 6806d7673c7e5802dc49e7a4aa3a6cd683d72eb0 Ash Wilson 1394307279 -0500 commit: More changes in branch. 16 | 6806d7673c7e5802dc49e7a4aa3a6cd683d72eb0 ab0b0263337e1bac55041d95c805cdb65b8434b4 Ash Wilson 1394307286 -0500 checkout: moving from branch to master 17 | ab0b0263337e1bac55041d95c805cdb65b8434b4 51d48cc54f3f108ed59c0c4d7470a81a6cb4896a Ash Wilson 1395272112 -0400 commit: subdir 18 | 51d48cc54f3f108ed59c0c4d7470a81a6cb4896a 6806d7673c7e5802dc49e7a4aa3a6cd683d72eb0 Ash Wilson 1397570145 -0400 checkout: moving from master to branch 19 | 6806d7673c7e5802dc49e7a4aa3a6cd683d72eb0 6806d7673c7e5802dc49e7a4aa3a6cd683d72eb0 Ash Wilson 1397570157 -0400 checkout: moving from branch to rebaser 20 | 6806d7673c7e5802dc49e7a4aa3a6cd683d72eb0 51d48cc54f3f108ed59c0c4d7470a81a6cb4896a Ash Wilson 1397570168 -0400 rebase: checkout master 21 | 51d48cc54f3f108ed59c0c4d7470a81a6cb4896a 6806d7673c7e5802dc49e7a4aa3a6cd683d72eb0 Ash Wilson 1397571353 -0400 rebase: aborting 22 | 6806d7673c7e5802dc49e7a4aa3a6cd683d72eb0 51d48cc54f3f108ed59c0c4d7470a81a6cb4896a Ash Wilson 1397571363 -0400 rebase -i (start): checkout master 23 | 51d48cc54f3f108ed59c0c4d7470a81a6cb4896a 6806d7673c7e5802dc49e7a4aa3a6cd683d72eb0 Ash Wilson 1397571382 -0400 rebase: aborting 24 | -------------------------------------------------------------------------------- /spec/fixtures/merging.git/logs/refs/heads/branch: -------------------------------------------------------------------------------- 1 | 0000000000000000000000000000000000000000 6e3541659dc7f506812b55ce4f6f9554a420464a Ash Wilson 1393692309 -0500 branch: Created from HEAD 2 | 6e3541659dc7f506812b55ce4f6f9554a420464a 205331815ca02331b81f804b81763ce4fed3d101 Ash Wilson 1393692327 -0500 commit: aaaa 3 | 205331815ca02331b81f804b81763ce4fed3d101 04a3e0388ccaa0722592d849e6777c4c74eeacba Ash Wilson 1393694611 -0500 commit: file1 mod 4 | 04a3e0388ccaa0722592d849e6777c4c74eeacba 6806d7673c7e5802dc49e7a4aa3a6cd683d72eb0 Ash Wilson 1394307279 -0500 commit: More changes in branch. 5 | -------------------------------------------------------------------------------- /spec/fixtures/merging.git/logs/refs/heads/master: -------------------------------------------------------------------------------- 1 | 0000000000000000000000000000000000000000 6e3541659dc7f506812b55ce4f6f9554a420464a Ash Wilson 1393692299 -0500 commit (initial): a 2 | 6e3541659dc7f506812b55ce4f6f9554a420464a 30fc72947114999af153eed9dc81729de90edc95 Ash Wilson 1393692344 -0500 commit: master 3 | 30fc72947114999af153eed9dc81729de90edc95 fc37b1879fff0ec01a96bf6792b973a2b1e1ee4d Ash Wilson 1393694629 -0500 commit: file2 mod 4 | fc37b1879fff0ec01a96bf6792b973a2b1e1ee4d 79b840d95746654d662782be57ffc9cdc607e458 Ash Wilson 1394307204 -0500 commit: More changes on the master side. 5 | 79b840d95746654d662782be57ffc9cdc607e458 ab0b0263337e1bac55041d95c805cdb65b8434b4 Ash Wilson 1394307232 -0500 commit: Changes in file2 as well. 6 | ab0b0263337e1bac55041d95c805cdb65b8434b4 51d48cc54f3f108ed59c0c4d7470a81a6cb4896a Ash Wilson 1395272112 -0400 commit: subdir 7 | -------------------------------------------------------------------------------- /spec/fixtures/merging.git/logs/refs/heads/rebaser: -------------------------------------------------------------------------------- 1 | 0000000000000000000000000000000000000000 6806d7673c7e5802dc49e7a4aa3a6cd683d72eb0 Ash Wilson 1397570151 -0400 branch: Created from branch 2 | -------------------------------------------------------------------------------- /spec/fixtures/merging.git/refs/heads/branch: -------------------------------------------------------------------------------- 1 | 6806d7673c7e5802dc49e7a4aa3a6cd683d72eb0 2 | -------------------------------------------------------------------------------- /spec/fixtures/merging.git/refs/heads/master: -------------------------------------------------------------------------------- 1 | 51d48cc54f3f108ed59c0c4d7470a81a6cb4896a 2 | -------------------------------------------------------------------------------- /spec/fixtures/merging.git/refs/heads/rebaser: -------------------------------------------------------------------------------- 1 | 6806d7673c7e5802dc49e7a4aa3a6cd683d72eb0 2 | -------------------------------------------------------------------------------- /spec/fixtures/multi-2way-diff.txt: -------------------------------------------------------------------------------- 1 | This is some text before the marking. 2 | 3 | More text. 4 | 5 | <<<<<<< HEAD 6 | My changes 7 | Multi-line even 8 | ======= 9 | Your changes 10 | >>>>>>> other-branch 11 | 12 | Text in between. 13 | 14 | <<<<<<< HEAD 15 | More of my changes 16 | ======= 17 | More of your changes 18 | >>>>>>> other-branch 19 | 20 | Stuff at the end. 21 | -------------------------------------------------------------------------------- /spec/fixtures/rebase-2way-diff.txt: -------------------------------------------------------------------------------- 1 | Before the start. 2 | 3 | <<<<<<< HEAD 4 | These are your changes 5 | ======= 6 | These are my changes 7 | >>>>>>> this is a message 8 | 9 | Past the end. 10 | -------------------------------------------------------------------------------- /spec/fixtures/rebasing.git/COMMIT_EDITMSG: -------------------------------------------------------------------------------- 1 | subdir 2 | -------------------------------------------------------------------------------- /spec/fixtures/rebasing.git/HEAD: -------------------------------------------------------------------------------- 1 | 51d48cc54f3f108ed59c0c4d7470a81a6cb4896a 2 | -------------------------------------------------------------------------------- /spec/fixtures/rebasing.git/ORIG_HEAD: -------------------------------------------------------------------------------- 1 | 6806d7673c7e5802dc49e7a4aa3a6cd683d72eb0 2 | -------------------------------------------------------------------------------- /spec/fixtures/rebasing.git/config: -------------------------------------------------------------------------------- 1 | [core] 2 | repositoryformatversion = 0 3 | filemode = true 4 | bare = false 5 | logallrefupdates = true 6 | ignorecase = true 7 | precomposeunicode = false 8 | -------------------------------------------------------------------------------- /spec/fixtures/rebasing.git/description: -------------------------------------------------------------------------------- 1 | Unnamed repository; edit this file 'description' to name the repository. 2 | -------------------------------------------------------------------------------- /spec/fixtures/rebasing.git/hooks/applypatch-msg.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to check the commit log message taken by 4 | # applypatch from an e-mail message. 5 | # 6 | # The hook should exit with non-zero status after issuing an 7 | # appropriate message if it wants to stop the commit. The hook is 8 | # allowed to edit the commit message file. 9 | # 10 | # To enable this hook, rename this file to "applypatch-msg". 11 | 12 | . git-sh-setup 13 | test -x "$GIT_DIR/hooks/commit-msg" && 14 | exec "$GIT_DIR/hooks/commit-msg" ${1+"$@"} 15 | : 16 | -------------------------------------------------------------------------------- /spec/fixtures/rebasing.git/hooks/commit-msg.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to check the commit log message. 4 | # Called by "git commit" with one argument, the name of the file 5 | # that has the commit message. The hook should exit with non-zero 6 | # status after issuing an appropriate message if it wants to stop the 7 | # commit. The hook is allowed to edit the commit message file. 8 | # 9 | # To enable this hook, rename this file to "commit-msg". 10 | 11 | # Uncomment the below to add a Signed-off-by line to the message. 12 | # Doing this in a hook is a bad idea in general, but the prepare-commit-msg 13 | # hook is more suited to it. 14 | # 15 | # SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') 16 | # grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" 17 | 18 | # This example catches duplicate Signed-off-by lines. 19 | 20 | test "" = "$(grep '^Signed-off-by: ' "$1" | 21 | sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || { 22 | echo >&2 Duplicate Signed-off-by lines. 23 | exit 1 24 | } 25 | -------------------------------------------------------------------------------- /spec/fixtures/rebasing.git/hooks/post-update.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to prepare a packed repository for use over 4 | # dumb transports. 5 | # 6 | # To enable this hook, rename this file to "post-update". 7 | 8 | exec git update-server-info 9 | -------------------------------------------------------------------------------- /spec/fixtures/rebasing.git/hooks/pre-applypatch.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to verify what is about to be committed 4 | # by applypatch from an e-mail message. 5 | # 6 | # The hook should exit with non-zero status after issuing an 7 | # appropriate message if it wants to stop the commit. 8 | # 9 | # To enable this hook, rename this file to "pre-applypatch". 10 | 11 | . git-sh-setup 12 | test -x "$GIT_DIR/hooks/pre-commit" && 13 | exec "$GIT_DIR/hooks/pre-commit" ${1+"$@"} 14 | : 15 | -------------------------------------------------------------------------------- /spec/fixtures/rebasing.git/hooks/pre-commit.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to verify what is about to be committed. 4 | # Called by "git commit" with no arguments. The hook should 5 | # exit with non-zero status after issuing an appropriate message if 6 | # it wants to stop the commit. 7 | # 8 | # To enable this hook, rename this file to "pre-commit". 9 | 10 | if git rev-parse --verify HEAD >/dev/null 2>&1 11 | then 12 | against=HEAD 13 | else 14 | # Initial commit: diff against an empty tree object 15 | against=4b825dc642cb6eb9a060e54bf8d69288fbee4904 16 | fi 17 | 18 | # If you want to allow non-ascii filenames set this variable to true. 19 | allownonascii=$(git config hooks.allownonascii) 20 | 21 | # Redirect output to stderr. 22 | exec 1>&2 23 | 24 | # Cross platform projects tend to avoid non-ascii filenames; prevent 25 | # them from being added to the repository. We exploit the fact that the 26 | # printable range starts at the space character and ends with tilde. 27 | if [ "$allownonascii" != "true" ] && 28 | # Note that the use of brackets around a tr range is ok here, (it's 29 | # even required, for portability to Solaris 10's /usr/bin/tr), since 30 | # the square bracket bytes happen to fall in the designated range. 31 | test $(git diff --cached --name-only --diff-filter=A -z $against | 32 | LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 33 | then 34 | echo "Error: Attempt to add a non-ascii file name." 35 | echo 36 | echo "This can cause problems if you want to work" 37 | echo "with people on other platforms." 38 | echo 39 | echo "To be portable it is advisable to rename the file ..." 40 | echo 41 | echo "If you know what you are doing you can disable this" 42 | echo "check using:" 43 | echo 44 | echo " git config hooks.allownonascii true" 45 | echo 46 | exit 1 47 | fi 48 | 49 | # If there are whitespace errors, print the offending file names and fail. 50 | exec git diff-index --check --cached $against -- 51 | -------------------------------------------------------------------------------- /spec/fixtures/rebasing.git/hooks/pre-push.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # An example hook script to verify what is about to be pushed. Called by "git 4 | # push" after it has checked the remote status, but before anything has been 5 | # pushed. If this script exits with a non-zero status nothing will be pushed. 6 | # 7 | # This hook is called with the following parameters: 8 | # 9 | # $1 -- Name of the remote to which the push is being done 10 | # $2 -- URL to which the push is being done 11 | # 12 | # If pushing without using a named remote those arguments will be equal. 13 | # 14 | # Information about the commits which are being pushed is supplied as lines to 15 | # the standard input in the form: 16 | # 17 | # 18 | # 19 | # This sample shows how to prevent push of commits where the log message starts 20 | # with "WIP" (work in progress). 21 | 22 | remote="$1" 23 | url="$2" 24 | 25 | z40=0000000000000000000000000000000000000000 26 | 27 | IFS=' ' 28 | while read local_ref local_sha remote_ref remote_sha 29 | do 30 | if [ "$local_sha" = $z40 ] 31 | then 32 | # Handle delete 33 | else 34 | if [ "$remote_sha" = $z40 ] 35 | then 36 | # New branch, examine all commits 37 | range="$local_sha" 38 | else 39 | # Update to existing branch, examine new commits 40 | range="$remote_sha..$local_sha" 41 | fi 42 | 43 | # Check for WIP commit 44 | commit=`git rev-list -n 1 --grep '^WIP' "$range"` 45 | if [ -n "$commit" ] 46 | then 47 | echo "Found WIP commit in $local_ref, not pushing" 48 | exit 1 49 | fi 50 | fi 51 | done 52 | 53 | exit 0 54 | -------------------------------------------------------------------------------- /spec/fixtures/rebasing.git/hooks/pre-rebase.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Copyright (c) 2006, 2008 Junio C Hamano 4 | # 5 | # The "pre-rebase" hook is run just before "git rebase" starts doing 6 | # its job, and can prevent the command from running by exiting with 7 | # non-zero status. 8 | # 9 | # The hook is called with the following parameters: 10 | # 11 | # $1 -- the upstream the series was forked from. 12 | # $2 -- the branch being rebased (or empty when rebasing the current branch). 13 | # 14 | # This sample shows how to prevent topic branches that are already 15 | # merged to 'next' branch from getting rebased, because allowing it 16 | # would result in rebasing already published history. 17 | 18 | publish=next 19 | basebranch="$1" 20 | if test "$#" = 2 21 | then 22 | topic="refs/heads/$2" 23 | else 24 | topic=`git symbolic-ref HEAD` || 25 | exit 0 ;# we do not interrupt rebasing detached HEAD 26 | fi 27 | 28 | case "$topic" in 29 | refs/heads/??/*) 30 | ;; 31 | *) 32 | exit 0 ;# we do not interrupt others. 33 | ;; 34 | esac 35 | 36 | # Now we are dealing with a topic branch being rebased 37 | # on top of master. Is it OK to rebase it? 38 | 39 | # Does the topic really exist? 40 | git show-ref -q "$topic" || { 41 | echo >&2 "No such branch $topic" 42 | exit 1 43 | } 44 | 45 | # Is topic fully merged to master? 46 | not_in_master=`git rev-list --pretty=oneline ^master "$topic"` 47 | if test -z "$not_in_master" 48 | then 49 | echo >&2 "$topic is fully merged to master; better remove it." 50 | exit 1 ;# we could allow it, but there is no point. 51 | fi 52 | 53 | # Is topic ever merged to next? If so you should not be rebasing it. 54 | only_next_1=`git rev-list ^master "^$topic" ${publish} | sort` 55 | only_next_2=`git rev-list ^master ${publish} | sort` 56 | if test "$only_next_1" = "$only_next_2" 57 | then 58 | not_in_topic=`git rev-list "^$topic" master` 59 | if test -z "$not_in_topic" 60 | then 61 | echo >&2 "$topic is already up-to-date with master" 62 | exit 1 ;# we could allow it, but there is no point. 63 | else 64 | exit 0 65 | fi 66 | else 67 | not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"` 68 | /usr/bin/perl -e ' 69 | my $topic = $ARGV[0]; 70 | my $msg = "* $topic has commits already merged to public branch:\n"; 71 | my (%not_in_next) = map { 72 | /^([0-9a-f]+) /; 73 | ($1 => 1); 74 | } split(/\n/, $ARGV[1]); 75 | for my $elem (map { 76 | /^([0-9a-f]+) (.*)$/; 77 | [$1 => $2]; 78 | } split(/\n/, $ARGV[2])) { 79 | if (!exists $not_in_next{$elem->[0]}) { 80 | if ($msg) { 81 | print STDERR $msg; 82 | undef $msg; 83 | } 84 | print STDERR " $elem->[1]\n"; 85 | } 86 | } 87 | ' "$topic" "$not_in_next" "$not_in_master" 88 | exit 1 89 | fi 90 | 91 | exit 0 92 | 93 | ################################################################ 94 | 95 | This sample hook safeguards topic branches that have been 96 | published from being rewound. 97 | 98 | The workflow assumed here is: 99 | 100 | * Once a topic branch forks from "master", "master" is never 101 | merged into it again (either directly or indirectly). 102 | 103 | * Once a topic branch is fully cooked and merged into "master", 104 | it is deleted. If you need to build on top of it to correct 105 | earlier mistakes, a new topic branch is created by forking at 106 | the tip of the "master". This is not strictly necessary, but 107 | it makes it easier to keep your history simple. 108 | 109 | * Whenever you need to test or publish your changes to topic 110 | branches, merge them into "next" branch. 111 | 112 | The script, being an example, hardcodes the publish branch name 113 | to be "next", but it is trivial to make it configurable via 114 | $GIT_DIR/config mechanism. 115 | 116 | With this workflow, you would want to know: 117 | 118 | (1) ... if a topic branch has ever been merged to "next". Young 119 | topic branches can have stupid mistakes you would rather 120 | clean up before publishing, and things that have not been 121 | merged into other branches can be easily rebased without 122 | affecting other people. But once it is published, you would 123 | not want to rewind it. 124 | 125 | (2) ... if a topic branch has been fully merged to "master". 126 | Then you can delete it. More importantly, you should not 127 | build on top of it -- other people may already want to 128 | change things related to the topic as patches against your 129 | "master", so if you need further changes, it is better to 130 | fork the topic (perhaps with the same name) afresh from the 131 | tip of "master". 132 | 133 | Let's look at this example: 134 | 135 | o---o---o---o---o---o---o---o---o---o "next" 136 | / / / / 137 | / a---a---b A / / 138 | / / / / 139 | / / c---c---c---c B / 140 | / / / \ / 141 | / / / b---b C \ / 142 | / / / / \ / 143 | ---o---o---o---o---o---o---o---o---o---o---o "master" 144 | 145 | 146 | A, B and C are topic branches. 147 | 148 | * A has one fix since it was merged up to "next". 149 | 150 | * B has finished. It has been fully merged up to "master" and "next", 151 | and is ready to be deleted. 152 | 153 | * C has not merged to "next" at all. 154 | 155 | We would want to allow C to be rebased, refuse A, and encourage 156 | B to be deleted. 157 | 158 | To compute (1): 159 | 160 | git rev-list ^master ^topic next 161 | git rev-list ^master next 162 | 163 | if these match, topic has not merged in next at all. 164 | 165 | To compute (2): 166 | 167 | git rev-list master..topic 168 | 169 | if this is empty, it is fully merged to "master". 170 | -------------------------------------------------------------------------------- /spec/fixtures/rebasing.git/hooks/prepare-commit-msg.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to prepare the commit log message. 4 | # Called by "git commit" with the name of the file that has the 5 | # commit message, followed by the description of the commit 6 | # message's source. The hook's purpose is to edit the commit 7 | # message file. If the hook fails with a non-zero status, 8 | # the commit is aborted. 9 | # 10 | # To enable this hook, rename this file to "prepare-commit-msg". 11 | 12 | # This hook includes three examples. The first comments out the 13 | # "Conflicts:" part of a merge commit. 14 | # 15 | # The second includes the output of "git diff --name-status -r" 16 | # into the message, just before the "git status" output. It is 17 | # commented because it doesn't cope with --amend or with squashed 18 | # commits. 19 | # 20 | # The third example adds a Signed-off-by line to the message, that can 21 | # still be edited. This is rarely a good idea. 22 | 23 | case "$2,$3" in 24 | merge,) 25 | /usr/bin/perl -i.bak -ne 's/^/# /, s/^# #/#/ if /^Conflicts/ .. /#/; print' "$1" ;; 26 | 27 | # ,|template,) 28 | # /usr/bin/perl -i.bak -pe ' 29 | # print "\n" . `git diff --cached --name-status -r` 30 | # if /^#/ && $first++ == 0' "$1" ;; 31 | 32 | *) ;; 33 | esac 34 | 35 | # SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') 36 | # grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" 37 | -------------------------------------------------------------------------------- /spec/fixtures/rebasing.git/hooks/update.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to blocks unannotated tags from entering. 4 | # Called by "git receive-pack" with arguments: refname sha1-old sha1-new 5 | # 6 | # To enable this hook, rename this file to "update". 7 | # 8 | # Config 9 | # ------ 10 | # hooks.allowunannotated 11 | # This boolean sets whether unannotated tags will be allowed into the 12 | # repository. By default they won't be. 13 | # hooks.allowdeletetag 14 | # This boolean sets whether deleting tags will be allowed in the 15 | # repository. By default they won't be. 16 | # hooks.allowmodifytag 17 | # This boolean sets whether a tag may be modified after creation. By default 18 | # it won't be. 19 | # hooks.allowdeletebranch 20 | # This boolean sets whether deleting branches will be allowed in the 21 | # repository. By default they won't be. 22 | # hooks.denycreatebranch 23 | # This boolean sets whether remotely creating branches will be denied 24 | # in the repository. By default this is allowed. 25 | # 26 | 27 | # --- Command line 28 | refname="$1" 29 | oldrev="$2" 30 | newrev="$3" 31 | 32 | # --- Safety check 33 | if [ -z "$GIT_DIR" ]; then 34 | echo "Don't run this script from the command line." >&2 35 | echo " (if you want, you could supply GIT_DIR then run" >&2 36 | echo " $0 )" >&2 37 | exit 1 38 | fi 39 | 40 | if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then 41 | echo "usage: $0 " >&2 42 | exit 1 43 | fi 44 | 45 | # --- Config 46 | allowunannotated=$(git config --bool hooks.allowunannotated) 47 | allowdeletebranch=$(git config --bool hooks.allowdeletebranch) 48 | denycreatebranch=$(git config --bool hooks.denycreatebranch) 49 | allowdeletetag=$(git config --bool hooks.allowdeletetag) 50 | allowmodifytag=$(git config --bool hooks.allowmodifytag) 51 | 52 | # check for no description 53 | projectdesc=$(sed -e '1q' "$GIT_DIR/description") 54 | case "$projectdesc" in 55 | "Unnamed repository"* | "") 56 | echo "*** Project description file hasn't been set" >&2 57 | exit 1 58 | ;; 59 | esac 60 | 61 | # --- Check types 62 | # if $newrev is 0000...0000, it's a commit to delete a ref. 63 | zero="0000000000000000000000000000000000000000" 64 | if [ "$newrev" = "$zero" ]; then 65 | newrev_type=delete 66 | else 67 | newrev_type=$(git cat-file -t $newrev) 68 | fi 69 | 70 | case "$refname","$newrev_type" in 71 | refs/tags/*,commit) 72 | # un-annotated tag 73 | short_refname=${refname##refs/tags/} 74 | if [ "$allowunannotated" != "true" ]; then 75 | echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2 76 | echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2 77 | exit 1 78 | fi 79 | ;; 80 | refs/tags/*,delete) 81 | # delete tag 82 | if [ "$allowdeletetag" != "true" ]; then 83 | echo "*** Deleting a tag is not allowed in this repository" >&2 84 | exit 1 85 | fi 86 | ;; 87 | refs/tags/*,tag) 88 | # annotated tag 89 | if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1 90 | then 91 | echo "*** Tag '$refname' already exists." >&2 92 | echo "*** Modifying a tag is not allowed in this repository." >&2 93 | exit 1 94 | fi 95 | ;; 96 | refs/heads/*,commit) 97 | # branch 98 | if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then 99 | echo "*** Creating a branch is not allowed in this repository" >&2 100 | exit 1 101 | fi 102 | ;; 103 | refs/heads/*,delete) 104 | # delete branch 105 | if [ "$allowdeletebranch" != "true" ]; then 106 | echo "*** Deleting a branch is not allowed in this repository" >&2 107 | exit 1 108 | fi 109 | ;; 110 | refs/remotes/*,commit) 111 | # tracking branch 112 | ;; 113 | refs/remotes/*,delete) 114 | # delete tracking branch 115 | if [ "$allowdeletebranch" != "true" ]; then 116 | echo "*** Deleting a tracking branch is not allowed in this repository" >&2 117 | exit 1 118 | fi 119 | ;; 120 | *) 121 | # Anything else (is there anything else?) 122 | echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2 123 | exit 1 124 | ;; 125 | esac 126 | 127 | # --- Finished 128 | exit 0 129 | -------------------------------------------------------------------------------- /spec/fixtures/rebasing.git/index: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smashwilson/merge-conflicts/0ce713e94cb8c08ccccf015d63c2653a589737e2/spec/fixtures/rebasing.git/index -------------------------------------------------------------------------------- /spec/fixtures/rebasing.git/info/exclude: -------------------------------------------------------------------------------- 1 | # git ls-files --others --exclude-from=.git/info/exclude 2 | # Lines that start with '#' are comments. 3 | # For a project mostly in C, the following would be a good set of 4 | # exclude patterns (uncomment them if you want to use them): 5 | # *.[oa] 6 | # *~ 7 | -------------------------------------------------------------------------------- /spec/fixtures/rebasing.git/logs/HEAD: -------------------------------------------------------------------------------- 1 | 0000000000000000000000000000000000000000 6e3541659dc7f506812b55ce4f6f9554a420464a Ash Wilson 1393692299 -0500 commit (initial): a 2 | 6e3541659dc7f506812b55ce4f6f9554a420464a 6e3541659dc7f506812b55ce4f6f9554a420464a Ash Wilson 1393692309 -0500 checkout: moving from master to branch 3 | 6e3541659dc7f506812b55ce4f6f9554a420464a 6e3541659dc7f506812b55ce4f6f9554a420464a Ash Wilson 1393692316 -0500 checkout: moving from branch to master 4 | 6e3541659dc7f506812b55ce4f6f9554a420464a 6e3541659dc7f506812b55ce4f6f9554a420464a Ash Wilson 1393692323 -0500 checkout: moving from master to branch 5 | 6e3541659dc7f506812b55ce4f6f9554a420464a 205331815ca02331b81f804b81763ce4fed3d101 Ash Wilson 1393692327 -0500 commit: aaaa 6 | 205331815ca02331b81f804b81763ce4fed3d101 6e3541659dc7f506812b55ce4f6f9554a420464a Ash Wilson 1393692335 -0500 checkout: moving from branch to master 7 | 6e3541659dc7f506812b55ce4f6f9554a420464a 30fc72947114999af153eed9dc81729de90edc95 Ash Wilson 1393692344 -0500 commit: master 8 | 30fc72947114999af153eed9dc81729de90edc95 205331815ca02331b81f804b81763ce4fed3d101 Ash Wilson 1393694601 -0500 checkout: moving from master to branch 9 | 205331815ca02331b81f804b81763ce4fed3d101 04a3e0388ccaa0722592d849e6777c4c74eeacba Ash Wilson 1393694611 -0500 commit: file1 mod 10 | 04a3e0388ccaa0722592d849e6777c4c74eeacba 30fc72947114999af153eed9dc81729de90edc95 Ash Wilson 1393694617 -0500 checkout: moving from branch to master 11 | 30fc72947114999af153eed9dc81729de90edc95 fc37b1879fff0ec01a96bf6792b973a2b1e1ee4d Ash Wilson 1393694629 -0500 commit: file2 mod 12 | fc37b1879fff0ec01a96bf6792b973a2b1e1ee4d 79b840d95746654d662782be57ffc9cdc607e458 Ash Wilson 1394307204 -0500 commit: More changes on the master side. 13 | 79b840d95746654d662782be57ffc9cdc607e458 ab0b0263337e1bac55041d95c805cdb65b8434b4 Ash Wilson 1394307232 -0500 commit: Changes in file2 as well. 14 | ab0b0263337e1bac55041d95c805cdb65b8434b4 04a3e0388ccaa0722592d849e6777c4c74eeacba Ash Wilson 1394307241 -0500 checkout: moving from master to branch 15 | 04a3e0388ccaa0722592d849e6777c4c74eeacba 6806d7673c7e5802dc49e7a4aa3a6cd683d72eb0 Ash Wilson 1394307279 -0500 commit: More changes in branch. 16 | 6806d7673c7e5802dc49e7a4aa3a6cd683d72eb0 ab0b0263337e1bac55041d95c805cdb65b8434b4 Ash Wilson 1394307286 -0500 checkout: moving from branch to master 17 | ab0b0263337e1bac55041d95c805cdb65b8434b4 51d48cc54f3f108ed59c0c4d7470a81a6cb4896a Ash Wilson 1395272112 -0400 commit: subdir 18 | 51d48cc54f3f108ed59c0c4d7470a81a6cb4896a 6806d7673c7e5802dc49e7a4aa3a6cd683d72eb0 Ash Wilson 1397570145 -0400 checkout: moving from master to branch 19 | 6806d7673c7e5802dc49e7a4aa3a6cd683d72eb0 6806d7673c7e5802dc49e7a4aa3a6cd683d72eb0 Ash Wilson 1397570157 -0400 checkout: moving from branch to rebaser 20 | 6806d7673c7e5802dc49e7a4aa3a6cd683d72eb0 51d48cc54f3f108ed59c0c4d7470a81a6cb4896a Ash Wilson 1397570168 -0400 rebase: checkout master 21 | -------------------------------------------------------------------------------- /spec/fixtures/rebasing.git/logs/refs/heads/branch: -------------------------------------------------------------------------------- 1 | 0000000000000000000000000000000000000000 6e3541659dc7f506812b55ce4f6f9554a420464a Ash Wilson 1393692309 -0500 branch: Created from HEAD 2 | 6e3541659dc7f506812b55ce4f6f9554a420464a 205331815ca02331b81f804b81763ce4fed3d101 Ash Wilson 1393692327 -0500 commit: aaaa 3 | 205331815ca02331b81f804b81763ce4fed3d101 04a3e0388ccaa0722592d849e6777c4c74eeacba Ash Wilson 1393694611 -0500 commit: file1 mod 4 | 04a3e0388ccaa0722592d849e6777c4c74eeacba 6806d7673c7e5802dc49e7a4aa3a6cd683d72eb0 Ash Wilson 1394307279 -0500 commit: More changes in branch. 5 | -------------------------------------------------------------------------------- /spec/fixtures/rebasing.git/logs/refs/heads/master: -------------------------------------------------------------------------------- 1 | 0000000000000000000000000000000000000000 6e3541659dc7f506812b55ce4f6f9554a420464a Ash Wilson 1393692299 -0500 commit (initial): a 2 | 6e3541659dc7f506812b55ce4f6f9554a420464a 30fc72947114999af153eed9dc81729de90edc95 Ash Wilson 1393692344 -0500 commit: master 3 | 30fc72947114999af153eed9dc81729de90edc95 fc37b1879fff0ec01a96bf6792b973a2b1e1ee4d Ash Wilson 1393694629 -0500 commit: file2 mod 4 | fc37b1879fff0ec01a96bf6792b973a2b1e1ee4d 79b840d95746654d662782be57ffc9cdc607e458 Ash Wilson 1394307204 -0500 commit: More changes on the master side. 5 | 79b840d95746654d662782be57ffc9cdc607e458 ab0b0263337e1bac55041d95c805cdb65b8434b4 Ash Wilson 1394307232 -0500 commit: Changes in file2 as well. 6 | ab0b0263337e1bac55041d95c805cdb65b8434b4 51d48cc54f3f108ed59c0c4d7470a81a6cb4896a Ash Wilson 1395272112 -0400 commit: subdir 7 | -------------------------------------------------------------------------------- /spec/fixtures/rebasing.git/logs/refs/heads/rebaser: -------------------------------------------------------------------------------- 1 | 0000000000000000000000000000000000000000 6806d7673c7e5802dc49e7a4aa3a6cd683d72eb0 Ash Wilson 1397570151 -0400 branch: Created from branch 2 | -------------------------------------------------------------------------------- /spec/fixtures/rebasing.git/rebase-apply/0001: -------------------------------------------------------------------------------- 1 | From 205331815ca02331b81f804b81763ce4fed3d101 Mon Sep 17 00:00:00 2001 2 | From: Ash Wilson 3 | Date: Sat, 1 Mar 2014 11:45:27 -0500 4 | Subject: aaaa 5 | 6 | --- 7 | file | 1 + 8 | 1 file changed, 1 insertion(+) 9 | 10 | diff --git a/file b/file 11 | index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..ccc3e7b48da0932cc0f7c4ce7b4fd834c7032fe1 100644 12 | --- a/file 13 | +++ b/file 14 | @@ -0,0 +1 @@ 15 | +aaaaa 16 | -- 17 | 1.9.1 18 | 19 | 20 | -------------------------------------------------------------------------------- /spec/fixtures/rebasing.git/rebase-apply/0002: -------------------------------------------------------------------------------- 1 | From 04a3e0388ccaa0722592d849e6777c4c74eeacba Mon Sep 17 00:00:00 2001 2 | From: Ash Wilson 3 | Date: Sat, 1 Mar 2014 12:23:31 -0500 4 | Subject: file1 mod 5 | 6 | --- 7 | file1 | 1 + 8 | 1 file changed, 1 insertion(+) 9 | create mode 100644 file1 10 | 11 | diff --git a/file1 b/file1 12 | new file mode 100644 13 | index 0000000000000000000000000000000000000000..8bd6648ed130ac9ece0f89cd9a8fbbfd2608427a 14 | --- /dev/null 15 | +++ b/file1 16 | @@ -0,0 +1 @@ 17 | +asdf 18 | -- 19 | 1.9.1 20 | 21 | 22 | -------------------------------------------------------------------------------- /spec/fixtures/rebasing.git/rebase-apply/0003: -------------------------------------------------------------------------------- 1 | From 6806d7673c7e5802dc49e7a4aa3a6cd683d72eb0 Mon Sep 17 00:00:00 2001 2 | From: Ash Wilson 3 | Date: Sat, 8 Mar 2014 14:34:39 -0500 4 | Subject: More changes in branch. 5 | 6 | --- 7 | file | 6 +++++- 8 | file2 | 6 ++++++ 9 | 2 files changed, 11 insertions(+), 1 deletion(-) 10 | 11 | diff --git a/file b/file 12 | index ccc3e7b48da0932cc0f7c4ce7b4fd834c7032fe1..f972a5b9c5b7600ad9a0d34621889ef74c083a9c 100644 13 | --- a/file 14 | +++ b/file 15 | @@ -1 +1,5 @@ 16 | -aaaaa 17 | +z 18 | +y 19 | +x 20 | +w 21 | +v 22 | diff --git a/file2 b/file2 23 | index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..ad7f66205e8370a15e2a11084e9cc11e318aa1d5 100644 24 | --- a/file2 25 | +++ b/file2 26 | @@ -0,0 +1,6 @@ 27 | +0 28 | +9 29 | +8 30 | +7 31 | +6 32 | +5 33 | -- 34 | 1.9.1 35 | 36 | -------------------------------------------------------------------------------- /spec/fixtures/rebasing.git/rebase-apply/abort-safety: -------------------------------------------------------------------------------- 1 | 51d48cc54f3f108ed59c0c4d7470a81a6cb4896a 2 | -------------------------------------------------------------------------------- /spec/fixtures/rebasing.git/rebase-apply/apply-opt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /spec/fixtures/rebasing.git/rebase-apply/author-script: -------------------------------------------------------------------------------- 1 | GIT_AUTHOR_NAME='Ash Wilson' 2 | GIT_AUTHOR_EMAIL='smashwilson@gmail.com' 3 | GIT_AUTHOR_DATE='@1393692327 -0500' 4 | -------------------------------------------------------------------------------- /spec/fixtures/rebasing.git/rebase-apply/final-commit: -------------------------------------------------------------------------------- 1 | aaaa 2 | -------------------------------------------------------------------------------- /spec/fixtures/rebasing.git/rebase-apply/head-name: -------------------------------------------------------------------------------- 1 | refs/heads/rebaser 2 | -------------------------------------------------------------------------------- /spec/fixtures/rebasing.git/rebase-apply/keep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /spec/fixtures/rebasing.git/rebase-apply/last: -------------------------------------------------------------------------------- 1 | 3 2 | -------------------------------------------------------------------------------- /spec/fixtures/rebasing.git/rebase-apply/msg-clean: -------------------------------------------------------------------------------- 1 | aaaa 2 | -------------------------------------------------------------------------------- /spec/fixtures/rebasing.git/rebase-apply/next: -------------------------------------------------------------------------------- 1 | 1 2 | -------------------------------------------------------------------------------- /spec/fixtures/rebasing.git/rebase-apply/no_inbody_headers: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /spec/fixtures/rebasing.git/rebase-apply/onto: -------------------------------------------------------------------------------- 1 | 51d48cc54f3f108ed59c0c4d7470a81a6cb4896a 2 | -------------------------------------------------------------------------------- /spec/fixtures/rebasing.git/rebase-apply/orig-head: -------------------------------------------------------------------------------- 1 | 6806d7673c7e5802dc49e7a4aa3a6cd683d72eb0 2 | -------------------------------------------------------------------------------- /spec/fixtures/rebasing.git/rebase-apply/original-commit: -------------------------------------------------------------------------------- 1 | 205331815ca02331b81f804b81763ce4fed3d101 2 | -------------------------------------------------------------------------------- /spec/fixtures/rebasing.git/rebase-apply/patch: -------------------------------------------------------------------------------- 1 | 205331815ca02331b81f804b81763ce4fed3d101 2 | diff --git a/file b/file 3 | index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..ccc3e7b48da0932cc0f7c4ce7b4fd834c7032fe1 100644 4 | --- a/file 5 | +++ b/file 6 | @@ -0,0 +1 @@ 7 | +aaaaa 8 | -------------------------------------------------------------------------------- /spec/fixtures/rebasing.git/rebase-apply/quiet: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /spec/fixtures/rebasing.git/rebase-apply/rebasing: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smashwilson/merge-conflicts/0ce713e94cb8c08ccccf015d63c2653a589737e2/spec/fixtures/rebasing.git/rebase-apply/rebasing -------------------------------------------------------------------------------- /spec/fixtures/rebasing.git/rebase-apply/scissors: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /spec/fixtures/rebasing.git/rebase-apply/sign: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /spec/fixtures/rebasing.git/rebase-apply/threeway: -------------------------------------------------------------------------------- 1 | t 2 | -------------------------------------------------------------------------------- /spec/fixtures/rebasing.git/rebase-apply/utf8: -------------------------------------------------------------------------------- 1 | t 2 | -------------------------------------------------------------------------------- /spec/fixtures/rebasing.git/refs/heads/branch: -------------------------------------------------------------------------------- 1 | 6806d7673c7e5802dc49e7a4aa3a6cd683d72eb0 2 | -------------------------------------------------------------------------------- /spec/fixtures/rebasing.git/refs/heads/master: -------------------------------------------------------------------------------- 1 | 51d48cc54f3f108ed59c0c4d7470a81a6cb4896a 2 | -------------------------------------------------------------------------------- /spec/fixtures/rebasing.git/refs/heads/rebaser: -------------------------------------------------------------------------------- 1 | 6806d7673c7e5802dc49e7a4aa3a6cd683d72eb0 2 | -------------------------------------------------------------------------------- /spec/fixtures/single-2way-diff.txt: -------------------------------------------------------------------------------- 1 | <<<<<<< HEAD 2 | These are my changes 3 | ======= 4 | These are your changes 5 | >>>>>>> master 6 | 7 | Past the end 8 | -------------------------------------------------------------------------------- /spec/fixtures/single-3way-diff-complex.txt: -------------------------------------------------------------------------------- 1 | <<<<<<< HEAD 2 | These are my changes 3 | ||||||| merged common ancestors 4 | <<<<<<< Temporary merge branch 1 5 | Refer: http://stackoverflow.com/questions/16990657/git-merge-diff3-style-need-explanation 6 | bbbbbb 7 | ======= 8 | cccccc 9 | >>>>>>> mybranch 10 | dddddd 11 | <<<<<<< HEAD 12 | eeeeee 13 | ||||||| merged common ancestors 14 | ffffff 15 | ||||||| merged common ancestors 16 | gggggg 17 | ======= 18 | >>>>>>> Temporary merge branch 2 19 | ======= 20 | These are your changes 21 | >>>>>>> master 22 | 23 | Past the end 24 | -------------------------------------------------------------------------------- /spec/fixtures/single-3way-diff.txt: -------------------------------------------------------------------------------- 1 | <<<<<<< HEAD 2 | These are my changes 3 | ||||||| merged common ancestors 4 | These are original texts 5 | ======= 6 | These are your changes 7 | >>>>>>> master 8 | 9 | Past the end 10 | -------------------------------------------------------------------------------- /spec/fixtures/triple-2way-diff.txt: -------------------------------------------------------------------------------- 1 | This is some text before the marking. 2 | 3 | More text. 4 | 5 | <<<<<<< HEAD 6 | My changes 7 | Multi-line even 8 | ======= 9 | Your changes 10 | >>>>>>> other-branch 11 | 12 | Text in between. 13 | 14 | <<<<<<< HEAD 15 | My middle changes 16 | ======= 17 | Your middle changes 18 | >>>>>>> other-branch 19 | 20 | Text in between. 21 | 22 | <<<<<<< HEAD 23 | More of my changes 24 | ======= 25 | More of your changes 26 | >>>>>>> other-branch 27 | 28 | Stuff at the very end. 29 | -------------------------------------------------------------------------------- /spec/git-shellout-spec.coffee: -------------------------------------------------------------------------------- 1 | GitOps = require '../lib/git/shellout' 2 | {BufferedProcess} = require 'atom' 3 | path = require 'path' 4 | 5 | describe 'GitBridge', -> 6 | 7 | gitWorkDir = "/fake/gitroot/" 8 | 9 | [context] = [] 10 | 11 | beforeEach -> 12 | atom.config.set('merge-conflicts.gitPath', '/usr/bin/git') 13 | 14 | waitsForPromise -> 15 | GitOps.getContext() 16 | .then (c) -> 17 | context = c 18 | context.workingDirPath = gitWorkDir 19 | 20 | it 'checks git status for merge conflicts', -> 21 | [c, a, o] = [] 22 | context.mockProcess ({command, args, options, stdout, stderr, exit}) -> 23 | [c, a, o] = [command, args, options] 24 | stdout('UU lib/file0.rb') 25 | stdout('AA lib/file1.rb') 26 | stdout('M lib/file2.rb') 27 | exit(0) 28 | { process: { on: (callback) -> } } 29 | 30 | conflicts = [] 31 | waitsForPromise -> 32 | context.readConflicts() 33 | .then (cs) -> 34 | conflicts = cs 35 | .catch (e) -> 36 | throw e 37 | 38 | runs -> 39 | expect(conflicts).toEqual([ 40 | { path: 'lib/file0.rb', message: 'both modified'} 41 | { path: 'lib/file1.rb', message: 'both added'} 42 | ]) 43 | expect(c).toBe('/usr/bin/git') 44 | expect(a).toEqual(['status', '--porcelain']) 45 | expect(o).toEqual({ cwd: gitWorkDir }) 46 | 47 | describe 'isResolvedFile', -> 48 | 49 | statusMeansStaged = (status, checkPath = 'lib/file2.txt') -> 50 | context.mockProcess ({stdout, exit}) -> 51 | stdout("#{status} lib/file2.txt") 52 | exit(0) 53 | { process: { on: (callback) -> } } 54 | 55 | context.isResolvedFile(checkPath) 56 | 57 | it 'is true if already resolved', -> 58 | waitsForPromise -> statusMeansStaged('M ').then (s) -> expect(s).toBe(true) 59 | 60 | it 'is true if resolved as ours', -> 61 | waitsForPromise -> statusMeansStaged(' M', 'lib/file1.txt').then (s) -> expect(s).toBe(true) 62 | 63 | it 'is false if still in conflict', -> 64 | waitsForPromise -> statusMeansStaged('UU').then (s) -> expect(s).toBe(false) 65 | 66 | it 'is false if resolved, but then modified', -> 67 | waitsForPromise -> statusMeansStaged('MM').then (s) -> expect(s).toBe(false) 68 | 69 | it 'checks out "our" version of a file from the index', -> 70 | [c, a, o] = [] 71 | context.mockProcess ({command, args, options, exit}) -> 72 | [c, a, o] = [command, args, options] 73 | exit(0) 74 | { process: { on: (callback) -> } } 75 | 76 | called = false 77 | waitsForPromise -> 78 | context.checkoutSide('ours', 'lib/file1.txt').then -> called = true 79 | 80 | runs -> 81 | expect(called).toBe(true) 82 | expect(c).toBe('/usr/bin/git') 83 | expect(a).toEqual(['checkout', '--ours', 'lib/file1.txt']) 84 | expect(o).toEqual({ cwd: gitWorkDir }) 85 | 86 | it 'stages changes to a file', -> 87 | p = "" 88 | context.repository.repo.add = (path) -> p = path 89 | 90 | called = false 91 | waitsForPromise -> 92 | context.resolveFile('lib/file1.txt').then -> called = true 93 | 94 | runs -> 95 | expect(called).toBe(true) 96 | expect(p).toBe('lib/file1.txt') 97 | 98 | describe 'rebase detection', -> 99 | 100 | withRoot = (gitDir, callback) -> 101 | fullDir = path.join atom.project.getDirectories()[0].getPath(), gitDir 102 | saved = context.repository.getPath 103 | context.repository.getPath = -> fullDir 104 | callback() 105 | context.repository.getPath = saved 106 | 107 | it 'recognizes a non-interactive rebase', -> 108 | withRoot 'rebasing.git', -> 109 | expect(context.isRebasing()).toBe(true) 110 | 111 | it 'recognizes an interactive rebase', -> 112 | withRoot 'irebasing.git', -> 113 | expect(context.isRebasing()).toBe(true) 114 | 115 | it 'returns false if not rebasing', -> 116 | withRoot 'merging.git', -> 117 | expect(context.isRebasing()).toBe(false) 118 | -------------------------------------------------------------------------------- /spec/util.coffee: -------------------------------------------------------------------------------- 1 | {Emitter} = require 'atom' 2 | 3 | module.exports = 4 | openPath: (path, callback) -> 5 | workspaceElement = atom.views.getView(atom.workspace) 6 | jasmine.attachToDOM(workspaceElement) 7 | 8 | waitsForPromise -> atom.workspace.open(path) 9 | 10 | runs -> 11 | callback(atom.views.getView(atom.workspace.getActivePaneItem())) 12 | 13 | rowRangeFrom: (marker) -> 14 | [marker.getTailBufferPosition().row, marker.getHeadBufferPosition().row] 15 | 16 | pkgEmitter: -> 17 | emitter = new Emitter 18 | 19 | return { 20 | onDidResolveConflict: (callback) -> emitter.on 'did-resolve-conflict', callback 21 | didResolveConflict: (event) -> emitter.emit 'did-resolve-conflict', event 22 | onDidResolveFile: (callback) -> emitter.on 'did-resolve-file', callback 23 | didResolveFile: (event) -> emitter.emit 'did-resolve-file', event 24 | onDidQuitConflictResolution: (callback) -> emitter.on 'did-quit-conflict-resolution', callback 25 | didQuitConflictResolution: -> emitter.emit 'did-quit-conflict-resolution' 26 | onDidCompleteConflictResolution: (callback) -> emitter.on 'did-complete-conflict-resolution', callback 27 | didCompleteConflictResolution: -> emitter.emit 'did-complete-conflict-resolution' 28 | dispose: -> emitter.dispose() 29 | } 30 | -------------------------------------------------------------------------------- /spec/view/merge-conflicts-view-spec.coffee: -------------------------------------------------------------------------------- 1 | {Directory} = require 'atom' 2 | path = require 'path' 3 | _ = require 'underscore-plus' 4 | 5 | {MergeConflictsView} = require '../../lib/view/merge-conflicts-view' 6 | 7 | {MergeState} = require '../../lib/merge-state' 8 | {Conflict} = require '../../lib/conflict' 9 | {GitOps} = require '../../lib/git' 10 | util = require '../util' 11 | 12 | describe 'MergeConflictsView', -> 13 | [view, context, state, pkg] = [] 14 | 15 | fullPath = (fname) -> 16 | path.join atom.project.getPaths()[0], 'path', fname 17 | 18 | repoPath = (fname) -> 19 | context.workingDirectory.relativize fullPath(fname) 20 | 21 | beforeEach -> 22 | pkg = util.pkgEmitter() 23 | 24 | workingDirectory = new Directory atom.project.getRepositories()[0].getWorkingDirectory() 25 | 26 | context = 27 | isRebase: false 28 | workingDirPath: workingDirectory.path 29 | workingDirectory: workingDirectory 30 | readConflicts: -> Promise.resolve conflicts 31 | checkoutSide: -> Promise.resolve() 32 | 33 | conflicts = _.map ['file1.txt', 'file2.txt'], (fname) -> 34 | { path: repoPath(fname), message: 'both modified' } 35 | 36 | util.openPath 'triple-2way-diff.txt', (editorView) -> 37 | state = new MergeState conflicts, context, false 38 | conflicts = Conflict.all state, editorView.getModel() 39 | 40 | view = new MergeConflictsView(state, pkg) 41 | 42 | afterEach -> 43 | pkg.dispose() 44 | 45 | describe 'conflict resolution progress', -> 46 | progressFor = (filename) -> 47 | view.pathList.find("li[data-path='#{repoPath filename}'] progress")[0] 48 | 49 | it 'starts at zero', -> 50 | expect(progressFor('file1.txt').value).toBe(0) 51 | expect(progressFor('file2.txt').value).toBe(0) 52 | 53 | it 'advances when requested', -> 54 | pkg.didResolveConflict 55 | file: fullPath('file1.txt'), 56 | total: 3, 57 | resolved: 2 58 | progress1 = progressFor 'file1.txt' 59 | expect(progress1.value).toBe(2) 60 | expect(progress1.max).toBe(3) 61 | 62 | describe 'tracking the progress of staging', -> 63 | 64 | isMarkedWith = (filename, icon) -> 65 | rs = view.pathList.find("li[data-path='#{repoPath filename}'] span.icon-#{icon}") 66 | rs.length isnt 0 67 | 68 | it 'starts without files marked as staged', -> 69 | expect(isMarkedWith 'file1.txt', 'dash').toBe(true) 70 | expect(isMarkedWith 'file2.txt', 'dash').toBe(true) 71 | 72 | it 'marks files as staged on events', -> 73 | context.readConflicts = -> Promise.resolve([{ path: repoPath("file2.txt"), message: "both modified"}]) 74 | 75 | pkg.didResolveFile file: fullPath('file1.txt') 76 | 77 | # Terrible hack. 78 | waitsFor -> isMarkedWith 'file1.txt', 'check' 79 | 80 | runs -> 81 | expect(isMarkedWith 'file1.txt', 'check').toBe(true) 82 | expect(isMarkedWith 'file2.txt', 'dash').toBe(true) 83 | 84 | it 'minimizes and restores the view on request', -> 85 | expect(view.hasClass 'minimized').toBe(false) 86 | view.minimize() 87 | expect(view.hasClass 'minimized').toBe(true) 88 | view.restore() 89 | expect(view.hasClass 'minimized').toBe(false) 90 | -------------------------------------------------------------------------------- /spec/view/navigation-view-spec.coffee: -------------------------------------------------------------------------------- 1 | {NavigationView} = require '../../lib/view/navigation-view' 2 | 3 | {Conflict} = require '../../lib/conflict' 4 | util = require '../util' 5 | 6 | describe 'NavigationView', -> 7 | [view, editorView, editor, conflicts, conflict] = [] 8 | 9 | beforeEach -> 10 | util.openPath "triple-2way-diff.txt", (v) -> 11 | editorView = v 12 | editor = editorView.getModel() 13 | conflicts = Conflict.all({}, editor) 14 | conflict = conflicts[1] 15 | 16 | view = new NavigationView(conflict.navigator, editor) 17 | 18 | it 'deletes the separator line on resolution', -> 19 | c.ours.resolve() for c in conflicts 20 | text = editor.getText() 21 | expect(text).not.toContain("My middle changes\n=======\nYour middle changes") 22 | 23 | it 'scrolls to the next diff', -> 24 | spyOn(editor, "setCursorBufferPosition") 25 | view.down() 26 | p = conflicts[2].ours.marker.getTailBufferPosition() 27 | expect(editor.setCursorBufferPosition).toHaveBeenCalledWith(p) 28 | 29 | it 'scrolls to the previous diff', -> 30 | spyOn(editor, "setCursorBufferPosition") 31 | view.up() 32 | p = conflicts[0].ours.marker.getTailBufferPosition() 33 | expect(editor.setCursorBufferPosition).toHaveBeenCalledWith(p) 34 | -------------------------------------------------------------------------------- /spec/view/resolver-view-spec.coffee: -------------------------------------------------------------------------------- 1 | {ResolverView} = require '../../lib/view/resolver-view' 2 | 3 | {GitOps} = require '../../lib/git' 4 | util = require '../util' 5 | 6 | describe 'ResolverView', -> 7 | [view, fakeEditor, pkg] = [] 8 | 9 | state = 10 | context: 11 | isResolvedFile: -> Promise.resolve false 12 | resolveFile: -> 13 | resolveText: "Stage" 14 | relativize: (filepath) -> filepath["/fake/gitroot/".length..] 15 | 16 | beforeEach -> 17 | pkg = util.pkgEmitter() 18 | fakeEditor = { 19 | isModified: -> true 20 | getURI: -> '/fake/gitroot/lib/file1.txt' 21 | save: -> 22 | onDidSave: -> 23 | } 24 | 25 | view = new ResolverView(fakeEditor, state, pkg) 26 | 27 | it 'begins needing both saving and staging', -> 28 | waitsForPromise -> view.refresh() 29 | runs -> expect(view.actionText.text()).toBe('Save and stage') 30 | 31 | it 'shows if the file only needs staged', -> 32 | fakeEditor.isModified = -> false 33 | waitsForPromise -> view.refresh() 34 | runs -> expect(view.actionText.text()).toBe('Stage') 35 | 36 | it 'saves and stages the file', -> 37 | p = null 38 | state.context.resolveFile = (filepath) -> 39 | p = filepath 40 | Promise.resolve() 41 | 42 | spyOn(fakeEditor, 'save') 43 | 44 | waitsForPromise -> view.resolve() 45 | 46 | runs -> 47 | expect(fakeEditor.save).toHaveBeenCalled() 48 | expect(p).toBe('lib/file1.txt') 49 | -------------------------------------------------------------------------------- /spec/view/side-view-spec.coffee: -------------------------------------------------------------------------------- 1 | {$} = require 'space-pen' 2 | {SideView} = require '../../lib/view/side-view' 3 | 4 | {Conflict} = require '../../lib/conflict' 5 | util = require '../util' 6 | 7 | describe 'SideView', -> 8 | [view, editorView, ours, theirs] = [] 9 | 10 | text = -> editorView.getModel().getText() 11 | 12 | beforeEach -> 13 | util.openPath "single-2way-diff.txt", (v) -> 14 | editor = v.getModel() 15 | editorView = v 16 | conflict = Conflict.all({ isRebase: false }, editor)[0] 17 | [ours, theirs] = [conflict.ours, conflict.theirs] 18 | view = new SideView(ours, editor) 19 | 20 | it 'applies its position as a CSS class', -> 21 | expect(view.hasClass 'top').toBe(true) 22 | expect(view.hasClass 'bottom').toBe(false) 23 | 24 | it 'knows if its text is unaltered', -> 25 | expect(ours.isDirty).toBe(false) 26 | expect(theirs.isDirty).toBe(false) 27 | 28 | describe 'when its text has been edited', -> 29 | [editor] = [] 30 | 31 | beforeEach -> 32 | editor = editorView.getModel() 33 | editor.setCursorBufferPosition [1, 0] 34 | editor.insertText "I won't keep them, but " 35 | view.detectDirty() 36 | 37 | it 'detects that its text has been edited', -> 38 | expect(ours.isDirty).toBe(true) 39 | 40 | it 'adds a .dirty class to the view', -> 41 | expect(view.hasClass 'dirty').toBe(true) 42 | 43 | it 'reverts its text back to the original on request', -> 44 | view.revert() 45 | view.detectDirty() 46 | t = editor.getTextInBufferRange ours.marker.getBufferRange() 47 | expect(t).toBe("These are my changes\n") 48 | expect(ours.isDirty).toBe(false) 49 | 50 | it 'triggers conflict resolution', -> 51 | spyOn(ours, "resolve") 52 | view.useMe() 53 | expect(ours.resolve).toHaveBeenCalled() 54 | 55 | describe 'when chosen as the resolution', -> 56 | 57 | beforeEach -> 58 | ours.resolve() 59 | 60 | it 'deletes the marker line', -> 61 | expect(text()).not.toContain("<<<<<<< HEAD") 62 | 63 | describe 'when not chosen as the resolution', -> 64 | 65 | beforeEach -> 66 | theirs.resolve() 67 | 68 | it 'deletes its lines', -> 69 | expect(text()).not.toContain("These are my changes") 70 | 71 | it 'deletes the marker line', -> 72 | expect(text()).not.toContain("<<<<<<< HEAD") 73 | -------------------------------------------------------------------------------- /styles/merge-conflicts.atom-text-editor.less: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | 3 | .conflict-resolved { 4 | background: @resolved-line-color; 5 | } 6 | 7 | .conflict-ours { 8 | background: @ours-line-color; 9 | &.cursor-line { background: @ours-current-color; } 10 | } 11 | 12 | .conflict-theirs { 13 | background: @theirs-line-color; 14 | &.cursor-line { background: @theirs-current-color; } 15 | } 16 | 17 | .conflict-base { 18 | background: @base-line-color; 19 | &.cursor-line { background: @base-current-color; } 20 | } 21 | 22 | .conflict-dirty { 23 | background: @dirty-line-color; 24 | &.cursor-line { background: @dirty-current-color; } 25 | } 26 | -------------------------------------------------------------------------------- /styles/merge-conflicts.less: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | 3 | .merge-conflicts { 4 | position: relative; 5 | z-index: 10; 6 | 7 | .conflict-list { 8 | max-height: 150px; 9 | overflow: scroll; 10 | } 11 | 12 | .navigate { 13 | cursor: pointer; 14 | 15 | &:hover { 16 | background-color: @background-color-highlight; 17 | } 18 | } 19 | 20 | span.icon-fold { 21 | cursor: pointer; 22 | } 23 | 24 | span.icon-unfold { 25 | display: none; 26 | cursor: pointer; 27 | } 28 | 29 | &.minimized { 30 | span.icon-fold { 31 | display: none; 32 | } 33 | span.icon-unfold { 34 | display: inline-block; 35 | } 36 | } 37 | } 38 | 39 | .merge-conflicts-message { 40 | cursor: pointer; 41 | } 42 | 43 | .resolver { 44 | &.save-needed { 45 | .save .text-success { display: none; } 46 | .save .text-status { display: inline-block; } 47 | .stage { display: none; } 48 | } 49 | 50 | &.stage-needed { 51 | .stage .text-success { display: none; } 52 | .stage .text-status { display: inline-block; } 53 | } 54 | 55 | .save .text-status { display: none; } 56 | .stage .text-success { display: none; } 57 | } 58 | 59 | // This targetting is temporary until SideViews work as overlay decorations. 60 | // See https://github.com/smashwilson/merge-conflicts/pull/93 for progress. 61 | atom-overlay { 62 | .side { 63 | padding: 0 20px; 64 | left: 0; 65 | right: 0; 66 | 67 | span { 68 | margin-left: 10px; 69 | } 70 | span.pull-right { 71 | clear: right; 72 | margin-right: 20px; 73 | } 74 | 75 | button.revert { 76 | display: none; 77 | } 78 | 79 | &.top { 80 | border-top-left-radius: 5px; 81 | border-top-right-radius: 5px; 82 | button { 83 | margin-top: 1px; 84 | } 85 | } 86 | 87 | &.bottom { 88 | border-bottom-left-radius: 5px; 89 | border-bottom-right-radius: 5px; 90 | button { 91 | margin-top: -1px; 92 | } 93 | } 94 | 95 | &.dirty { 96 | background-color: @ui-site-color-5; 97 | 98 | button.revert { 99 | display: inline-block; 100 | } 101 | } 102 | } 103 | 104 | .navigation { 105 | padding: 0 20px; 106 | left: 0; 107 | right: 0; 108 | background: @base-background-color; 109 | 110 | span.pull-right { 111 | clear: right; 112 | margin-right: 20px; 113 | } 114 | 115 | button { 116 | margin-left: 5px; 117 | margin-top: -15px; 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /styles/variables.less: -------------------------------------------------------------------------------- 1 | @import "ui-variables"; 2 | 3 | @ours-line-color: fade(@ui-site-color-1, 40%); 4 | @ours-current-color: fadeout(@ours-line-color, 10%); 5 | @theirs-line-color: fade(@ui-site-color-2, 40%); 6 | @theirs-current-color: fadeout(@theirs-line-color, 10%); 7 | @base-line-color: fade(@ui-site-color-3, 40%); 8 | @base-current-color: fadeout(@base-line-color, 10%); 9 | @dirty-line-color: fade(@ui-site-color-5, 40%); 10 | @dirty-current-color: fadeout(@dirty-line-color, 10%); 11 | @resolved-line-color: @background-color-highlight; 12 | --------------------------------------------------------------------------------