| 1 | """Performs a 3-way merge for GIT files |
| 2 | """ |
| 3 | |
| 4 | __copyright__ = """ |
| 5 | Copyright (C) 2006, Catalin Marinas <catalin.marinas@gmail.com> |
| 6 | |
| 7 | This program is free software; you can redistribute it and/or modify |
| 8 | it under the terms of the GNU General Public License version 2 as |
| 9 | published by the Free Software Foundation. |
| 10 | |
| 11 | This program is distributed in the hope that it will be useful, |
| 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 14 | GNU General Public License for more details. |
| 15 | |
| 16 | You should have received a copy of the GNU General Public License |
| 17 | along with this program; if not, write to the Free Software |
| 18 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
| 19 | """ |
| 20 | |
| 21 | import sys, os |
| 22 | from stgit import basedir |
| 23 | from stgit.config import config, file_extensions |
| 24 | from stgit.utils import append_string |
| 25 | |
| 26 | |
| 27 | class GitMergeException(Exception): |
| 28 | pass |
| 29 | |
| 30 | |
| 31 | # |
| 32 | # Options |
| 33 | # |
| 34 | try: |
| 35 | merger = config.get('stgit', 'merger') |
| 36 | keeporig = config.get('stgit', 'keeporig') |
| 37 | except Exception, err: |
| 38 | raise GitMergeException, 'Configuration error: %s' % err |
| 39 | |
| 40 | |
| 41 | # |
| 42 | # Utility functions |
| 43 | # |
| 44 | def __str2none(x): |
| 45 | if x == '': |
| 46 | return None |
| 47 | else: |
| 48 | return x |
| 49 | |
| 50 | def __output(cmd): |
| 51 | f = os.popen(cmd, 'r') |
| 52 | string = f.readline().rstrip() |
| 53 | if f.close(): |
| 54 | raise GitMergeException, 'Error: failed to execute "%s"' % cmd |
| 55 | return string |
| 56 | |
| 57 | def __checkout_files(orig_hash, file1_hash, file2_hash, |
| 58 | path, |
| 59 | orig_mode, file1_mode, file2_mode): |
| 60 | """Check out the files passed as arguments |
| 61 | """ |
| 62 | global orig, src1, src2 |
| 63 | |
| 64 | if orig_hash: |
| 65 | orig = path + file_extensions()['ancestor'] |
| 66 | tmp = __output('git-unpack-file %s' % orig_hash) |
| 67 | os.chmod(tmp, int(orig_mode, 8)) |
| 68 | os.renames(tmp, orig) |
| 69 | if file1_hash: |
| 70 | src1 = path + file_extensions()['current'] |
| 71 | tmp = __output('git-unpack-file %s' % file1_hash) |
| 72 | os.chmod(tmp, int(file1_mode, 8)) |
| 73 | os.renames(tmp, src1) |
| 74 | if file2_hash: |
| 75 | src2 = path + file_extensions()['patched'] |
| 76 | tmp = __output('git-unpack-file %s' % file2_hash) |
| 77 | os.chmod(tmp, int(file2_mode, 8)) |
| 78 | os.renames(tmp, src2) |
| 79 | |
| 80 | def __remove_files(orig_hash, file1_hash, file2_hash): |
| 81 | """Remove any temporary files |
| 82 | """ |
| 83 | if orig_hash: |
| 84 | os.remove(orig) |
| 85 | if file1_hash: |
| 86 | os.remove(src1) |
| 87 | if file2_hash: |
| 88 | os.remove(src2) |
| 89 | pass |
| 90 | |
| 91 | def __conflict(path): |
| 92 | """Write the conflict file for the 'path' variable and exit |
| 93 | """ |
| 94 | append_string(os.path.join(basedir.get(), 'conflicts'), path) |
| 95 | |
| 96 | |
| 97 | # |
| 98 | # Main algorithm |
| 99 | # |
| 100 | def merge(orig_hash, file1_hash, file2_hash, |
| 101 | path, |
| 102 | orig_mode, file1_mode, file2_mode): |
| 103 | """Three-way merge for one file algorithm |
| 104 | """ |
| 105 | __checkout_files(orig_hash, file1_hash, file2_hash, |
| 106 | path, |
| 107 | orig_mode, file1_mode, file2_mode) |
| 108 | |
| 109 | # file exists in origin |
| 110 | if orig_hash: |
| 111 | # modified in both |
| 112 | if file1_hash and file2_hash: |
| 113 | # if modes are the same (git-read-tree probably dealt with it) |
| 114 | if file1_hash == file2_hash: |
| 115 | if os.system('git-update-index --cacheinfo %s %s %s' |
| 116 | % (file1_mode, file1_hash, path)) != 0: |
| 117 | print >> sys.stderr, 'Error: git-update-index failed' |
| 118 | __conflict(path) |
| 119 | return 1 |
| 120 | if os.system('git-checkout-index -u -f -- %s' % path): |
| 121 | print >> sys.stderr, 'Error: git-checkout-index failed' |
| 122 | __conflict(path) |
| 123 | return 1 |
| 124 | if file1_mode != file2_mode: |
| 125 | print >> sys.stderr, \ |
| 126 | 'Error: File added in both, permissions conflict' |
| 127 | __conflict(path) |
| 128 | return 1 |
| 129 | # 3-way merge |
| 130 | else: |
| 131 | merge_ok = os.system(merger % {'branch1': src1, |
| 132 | 'ancestor': orig, |
| 133 | 'branch2': src2, |
| 134 | 'output': path }) == 0 |
| 135 | |
| 136 | if merge_ok: |
| 137 | os.system('git-update-index -- %s' % path) |
| 138 | __remove_files(orig_hash, file1_hash, file2_hash) |
| 139 | return 0 |
| 140 | else: |
| 141 | print >> sys.stderr, \ |
| 142 | 'Error: three-way merge tool failed for file "%s"' \ |
| 143 | % path |
| 144 | # reset the cache to the first branch |
| 145 | os.system('git-update-index --cacheinfo %s %s %s' |
| 146 | % (file1_mode, file1_hash, path)) |
| 147 | if keeporig != 'yes': |
| 148 | __remove_files(orig_hash, file1_hash, file2_hash) |
| 149 | __conflict(path) |
| 150 | return 1 |
| 151 | # file deleted in both or deleted in one and unchanged in the other |
| 152 | elif not (file1_hash or file2_hash) \ |
| 153 | or file1_hash == orig_hash or file2_hash == orig_hash: |
| 154 | if os.path.exists(path): |
| 155 | os.remove(path) |
| 156 | __remove_files(orig_hash, file1_hash, file2_hash) |
| 157 | return os.system('git-update-index --remove -- %s' % path) |
| 158 | # file deleted in one and changed in the other |
| 159 | else: |
| 160 | # Do something here - we must at least merge the entry in |
| 161 | # the cache, instead of leaving it in U(nmerged) state. In |
| 162 | # fact, stg resolved does not handle that. |
| 163 | |
| 164 | # Do the same thing cogito does - remove the file in any case. |
| 165 | os.system('git-update-index --remove -- %s' % path) |
| 166 | |
| 167 | #if file1_hash: |
| 168 | ## file deleted upstream and changed in the patch. The |
| 169 | ## patch is probably going to move the changes |
| 170 | ## elsewhere. |
| 171 | |
| 172 | #os.system('git-update-index --remove -- %s' % path) |
| 173 | #else: |
| 174 | ## file deleted in the patch and changed upstream. We |
| 175 | ## could re-delete it, but for now leave it there - |
| 176 | ## and let the user check if he still wants to remove |
| 177 | ## the file. |
| 178 | |
| 179 | ## reset the cache to the first branch |
| 180 | #os.system('git-update-index --cacheinfo %s %s %s' |
| 181 | # % (file1_mode, file1_hash, path)) |
| 182 | __conflict(path) |
| 183 | return 1 |
| 184 | |
| 185 | # file does not exist in origin |
| 186 | else: |
| 187 | # file added in both |
| 188 | if file1_hash and file2_hash: |
| 189 | # files are the same |
| 190 | if file1_hash == file2_hash: |
| 191 | if os.system('git-update-index --add --cacheinfo %s %s %s' |
| 192 | % (file1_mode, file1_hash, path)) != 0: |
| 193 | print >> sys.stderr, 'Error: git-update-index failed' |
| 194 | __conflict(path) |
| 195 | return 1 |
| 196 | if os.system('git-checkout-index -u -f -- %s' % path): |
| 197 | print >> sys.stderr, 'Error: git-checkout-index failed' |
| 198 | __conflict(path) |
| 199 | return 1 |
| 200 | if file1_mode != file2_mode: |
| 201 | print >> sys.stderr, \ |
| 202 | 'Error: File "s" added in both, ' \ |
| 203 | 'permissions conflict' % path |
| 204 | __conflict(path) |
| 205 | return 1 |
| 206 | # files are different |
| 207 | else: |
| 208 | # reset the index to the current file |
| 209 | os.system('git-update-index -- %s' % path) |
| 210 | print >> sys.stderr, \ |
| 211 | 'Error: File "%s" added in branches but different' % path |
| 212 | __conflict(path) |
| 213 | return 1 |
| 214 | # file added in one |
| 215 | elif file1_hash or file2_hash: |
| 216 | if file1_hash: |
| 217 | mode = file1_mode |
| 218 | obj = file1_hash |
| 219 | else: |
| 220 | mode = file2_mode |
| 221 | obj = file2_hash |
| 222 | if os.system('git-update-index --add --cacheinfo %s %s %s' |
| 223 | % (mode, obj, path)) != 0: |
| 224 | print >> sys.stderr, 'Error: git-update-index failed' |
| 225 | __conflict(path) |
| 226 | return 1 |
| 227 | __remove_files(orig_hash, file1_hash, file2_hash) |
| 228 | return os.system('git-checkout-index -u -f -- %s' % path) |
| 229 | |
| 230 | # Unhandled case |
| 231 | print >> sys.stderr, 'Error: Unhandled merge conflict: ' \ |
| 232 | '"%s" "%s" "%s" "%s" "%s" "%s" "%s"' \ |
| 233 | % (orig_hash, file1_hash, file2_hash, |
| 234 | path, |
| 235 | orig_mode, file1_mode, file2_mode) |
| 236 | __conflict(path) |
| 237 | return 1 |