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