##// END OF EJS Templates
git: fix index handling of removed files during commit (issue6398)...
Augie Fackler -
r45992:d4cf8034 default
parent child Browse files
Show More
@@ -1,339 +1,341 b''
1 1 from __future__ import absolute_import
2 2
3 3 import contextlib
4 4 import errno
5 5 import os
6 6
7 7 from mercurial import (
8 8 error,
9 9 extensions,
10 10 match as matchmod,
11 11 node as nodemod,
12 12 pycompat,
13 13 scmutil,
14 14 util,
15 15 )
16 16 from mercurial.interfaces import (
17 17 dirstate as intdirstate,
18 18 util as interfaceutil,
19 19 )
20 20
21 21 from . import gitutil
22 22
23 23 pygit2 = gitutil.get_pygit2()
24 24
25 25
26 26 def readpatternfile(orig, filepath, warn, sourceinfo=False):
27 27 if not (b'info/exclude' in filepath or filepath.endswith(b'.gitignore')):
28 28 return orig(filepath, warn, sourceinfo=False)
29 29 result = []
30 30 warnings = []
31 31 with open(filepath, b'rb') as fp:
32 32 for l in fp:
33 33 l = l.strip()
34 34 if not l or l.startswith(b'#'):
35 35 continue
36 36 if l.startswith(b'!'):
37 37 warnings.append(b'unsupported ignore pattern %s' % l)
38 38 continue
39 39 if l.startswith(b'/'):
40 40 result.append(b'rootglob:' + l[1:])
41 41 else:
42 42 result.append(b'relglob:' + l)
43 43 return result, warnings
44 44
45 45
46 46 extensions.wrapfunction(matchmod, b'readpatternfile', readpatternfile)
47 47
48 48
49 49 _STATUS_MAP = {}
50 50 if pygit2:
51 51 _STATUS_MAP = {
52 52 pygit2.GIT_STATUS_CONFLICTED: b'm',
53 53 pygit2.GIT_STATUS_CURRENT: b'n',
54 54 pygit2.GIT_STATUS_IGNORED: b'?',
55 55 pygit2.GIT_STATUS_INDEX_DELETED: b'r',
56 56 pygit2.GIT_STATUS_INDEX_MODIFIED: b'n',
57 57 pygit2.GIT_STATUS_INDEX_NEW: b'a',
58 58 pygit2.GIT_STATUS_INDEX_RENAMED: b'a',
59 59 pygit2.GIT_STATUS_INDEX_TYPECHANGE: b'n',
60 60 pygit2.GIT_STATUS_WT_DELETED: b'r',
61 61 pygit2.GIT_STATUS_WT_MODIFIED: b'n',
62 62 pygit2.GIT_STATUS_WT_NEW: b'?',
63 63 pygit2.GIT_STATUS_WT_RENAMED: b'a',
64 64 pygit2.GIT_STATUS_WT_TYPECHANGE: b'n',
65 65 pygit2.GIT_STATUS_WT_UNREADABLE: b'?',
66 66 pygit2.GIT_STATUS_INDEX_MODIFIED | pygit2.GIT_STATUS_WT_MODIFIED: 'm',
67 67 }
68 68
69 69
70 70 @interfaceutil.implementer(intdirstate.idirstate)
71 71 class gitdirstate(object):
72 72 def __init__(self, ui, root, gitrepo):
73 73 self._ui = ui
74 74 self._root = os.path.dirname(root)
75 75 self.git = gitrepo
76 76 self._plchangecallbacks = {}
77 77
78 78 def p1(self):
79 79 try:
80 80 return self.git.head.peel().id.raw
81 81 except pygit2.GitError:
82 82 # Typically happens when peeling HEAD fails, as in an
83 83 # empty repository.
84 84 return nodemod.nullid
85 85
86 86 def p2(self):
87 87 # TODO: MERGE_HEAD? something like that, right?
88 88 return nodemod.nullid
89 89
90 90 def setparents(self, p1, p2=nodemod.nullid):
91 91 assert p2 == nodemod.nullid, b'TODO merging support'
92 92 self.git.head.set_target(gitutil.togitnode(p1))
93 93
94 94 @util.propertycache
95 95 def identity(self):
96 96 return util.filestat.frompath(
97 97 os.path.join(self._root, b'.git', b'index')
98 98 )
99 99
100 100 def branch(self):
101 101 return b'default'
102 102
103 103 def parents(self):
104 104 # TODO how on earth do we find p2 if a merge is in flight?
105 105 return self.p1(), nodemod.nullid
106 106
107 107 def __iter__(self):
108 108 return (pycompat.fsencode(f.path) for f in self.git.index)
109 109
110 110 def items(self):
111 111 for ie in self.git.index:
112 112 yield ie.path, None # value should be a dirstatetuple
113 113
114 114 # py2,3 compat forward
115 115 iteritems = items
116 116
117 117 def __getitem__(self, filename):
118 118 try:
119 119 gs = self.git.status_file(filename)
120 120 except KeyError:
121 121 return b'?'
122 122 return _STATUS_MAP[gs]
123 123
124 124 def __contains__(self, filename):
125 125 try:
126 126 gs = self.git.status_file(filename)
127 127 return _STATUS_MAP[gs] != b'?'
128 128 except KeyError:
129 129 return False
130 130
131 131 def status(self, match, subrepos, ignored, clean, unknown):
132 132 listignored, listclean, listunknown = ignored, clean, unknown
133 133 # TODO handling of clean files - can we get that from git.status()?
134 134 modified, added, removed, deleted, unknown, ignored, clean = (
135 135 [],
136 136 [],
137 137 [],
138 138 [],
139 139 [],
140 140 [],
141 141 [],
142 142 )
143 143 gstatus = self.git.status()
144 144 for path, status in gstatus.items():
145 145 path = pycompat.fsencode(path)
146 146 if not match(path):
147 147 continue
148 148 if status == pygit2.GIT_STATUS_IGNORED:
149 149 if path.endswith(b'/'):
150 150 continue
151 151 ignored.append(path)
152 152 elif status in (
153 153 pygit2.GIT_STATUS_WT_MODIFIED,
154 154 pygit2.GIT_STATUS_INDEX_MODIFIED,
155 155 pygit2.GIT_STATUS_WT_MODIFIED
156 156 | pygit2.GIT_STATUS_INDEX_MODIFIED,
157 157 ):
158 158 modified.append(path)
159 159 elif status == pygit2.GIT_STATUS_INDEX_NEW:
160 160 added.append(path)
161 161 elif status == pygit2.GIT_STATUS_WT_NEW:
162 162 unknown.append(path)
163 163 elif status == pygit2.GIT_STATUS_WT_DELETED:
164 164 deleted.append(path)
165 165 elif status == pygit2.GIT_STATUS_INDEX_DELETED:
166 166 removed.append(path)
167 167 else:
168 168 raise error.Abort(
169 169 b'unhandled case: status for %r is %r' % (path, status)
170 170 )
171 171
172 172 if listclean:
173 173 observed = set(
174 174 modified + added + removed + deleted + unknown + ignored
175 175 )
176 176 index = self.git.index
177 177 index.read()
178 178 for entry in index:
179 179 path = pycompat.fsencode(entry.path)
180 180 if not match(path):
181 181 continue
182 182 if path in observed:
183 183 continue # already in some other set
184 184 if path[-1] == b'/':
185 185 continue # directory
186 186 clean.append(path)
187 187
188 188 # TODO are we really always sure of status here?
189 189 return (
190 190 False,
191 191 scmutil.status(
192 192 modified, added, removed, deleted, unknown, ignored, clean
193 193 ),
194 194 )
195 195
196 196 def flagfunc(self, buildfallback):
197 197 # TODO we can do better
198 198 return buildfallback()
199 199
200 200 def getcwd(self):
201 201 # TODO is this a good way to do this?
202 202 return os.path.dirname(
203 203 os.path.dirname(pycompat.fsencode(self.git.path))
204 204 )
205 205
206 206 def normalize(self, path):
207 207 normed = util.normcase(path)
208 208 assert normed == path, b"TODO handling of case folding: %s != %s" % (
209 209 normed,
210 210 path,
211 211 )
212 212 return path
213 213
214 214 @property
215 215 def _checklink(self):
216 216 return util.checklink(os.path.dirname(pycompat.fsencode(self.git.path)))
217 217
218 218 def copies(self):
219 219 # TODO support copies?
220 220 return {}
221 221
222 222 # # TODO what the heck is this
223 223 _filecache = set()
224 224
225 225 def pendingparentchange(self):
226 226 # TODO: we need to implement the context manager bits and
227 227 # correctly stage/revert index edits.
228 228 return False
229 229
230 230 def write(self, tr):
231 231 # TODO: call parent change callbacks
232 232
233 233 if tr:
234 234
235 235 def writeinner(category):
236 236 self.git.index.write()
237 237
238 238 tr.addpending(b'gitdirstate', writeinner)
239 239 else:
240 240 self.git.index.write()
241 241
242 242 def pathto(self, f, cwd=None):
243 243 if cwd is None:
244 244 cwd = self.getcwd()
245 245 # TODO core dirstate does something about slashes here
246 246 assert isinstance(f, bytes)
247 247 r = util.pathto(self._root, cwd, f)
248 248 return r
249 249
250 250 def matches(self, match):
251 251 for x in self.git.index:
252 252 p = pycompat.fsencode(x.path)
253 253 if match(p):
254 254 yield p
255 255
256 256 def normal(self, f, parentfiledata=None):
257 257 """Mark a file normal and clean."""
258 258 # TODO: for now we just let libgit2 re-stat the file. We can
259 259 # clearly do better.
260 260
261 261 def normallookup(self, f):
262 262 """Mark a file normal, but possibly dirty."""
263 263 # TODO: for now we just let libgit2 re-stat the file. We can
264 264 # clearly do better.
265 265
266 266 def walk(self, match, subrepos, unknown, ignored, full=True):
267 267 # TODO: we need to use .status() and not iterate the index,
268 268 # because the index doesn't force a re-walk and so `hg add` of
269 269 # a new file without an intervening call to status will
270 270 # silently do nothing.
271 271 r = {}
272 272 cwd = self.getcwd()
273 273 for path, status in self.git.status().items():
274 274 if path.startswith('.hg/'):
275 275 continue
276 276 path = pycompat.fsencode(path)
277 277 if not match(path):
278 278 continue
279 279 # TODO construct the stat info from the status object?
280 280 try:
281 281 s = os.stat(os.path.join(cwd, path))
282 282 except OSError as e:
283 283 if e.errno != errno.ENOENT:
284 284 raise
285 285 continue
286 286 r[path] = s
287 287 return r
288 288
289 289 def savebackup(self, tr, backupname):
290 290 # TODO: figure out a strategy for saving index backups.
291 291 pass
292 292
293 293 def restorebackup(self, tr, backupname):
294 294 # TODO: figure out a strategy for saving index backups.
295 295 pass
296 296
297 297 def add(self, f):
298 298 index = self.git.index
299 299 index.read()
300 300 index.add(pycompat.fsdecode(f))
301 301 index.write()
302 302
303 303 def drop(self, f):
304 304 index = self.git.index
305 305 index.read()
306 index.remove(pycompat.fsdecode(f))
307 index.write()
306 fs = pycompat.fsdecode(f)
307 if fs in index:
308 index.remove(fs)
309 index.write()
308 310
309 311 def remove(self, f):
310 312 index = self.git.index
311 313 index.read()
312 314 index.remove(pycompat.fsdecode(f))
313 315 index.write()
314 316
315 317 def copied(self, path):
316 318 # TODO: track copies?
317 319 return None
318 320
319 321 def prefetch_parents(self):
320 322 # TODO
321 323 pass
322 324
323 325 @contextlib.contextmanager
324 326 def parentchange(self):
325 327 # TODO: track this maybe?
326 328 yield
327 329
328 330 def addparentchangecallback(self, category, callback):
329 331 # TODO: should this be added to the dirstate interface?
330 332 self._plchangecallbacks[category] = callback
331 333
332 334 def clearbackup(self, tr, backupname):
333 335 # TODO
334 336 pass
335 337
336 338 def setbranch(self, branch):
337 339 raise error.Abort(
338 340 b'git repos do not support branches. try using bookmarks'
339 341 )
@@ -1,272 +1,277 b''
1 1 #require pygit2
2 2
3 3 Setup:
4 4 $ GIT_AUTHOR_NAME='test'; export GIT_AUTHOR_NAME
5 5 > GIT_AUTHOR_EMAIL='test@example.org'; export GIT_AUTHOR_EMAIL
6 6 > GIT_AUTHOR_DATE="2007-01-01 00:00:00 +0000"; export GIT_AUTHOR_DATE
7 7 > GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME"; export GIT_COMMITTER_NAME
8 8 > GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL"; export GIT_COMMITTER_EMAIL
9 9 > GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE"; export GIT_COMMITTER_DATE
10 10 > count=10
11 11 > gitcommit() {
12 12 > GIT_AUTHOR_DATE="2007-01-01 00:00:$count +0000";
13 13 > GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE"
14 14 > git commit "$@" >/dev/null 2>/dev/null || echo "git commit error"
15 15 > count=`expr $count + 1`
16 16 > }
17 17
18 18
19 19 Test auto-loading extension works:
20 20 $ mkdir nogit
21 21 $ cd nogit
22 22 $ mkdir .hg
23 23 $ echo git >> .hg/requires
24 24 $ hg status
25 25 abort: repository specified git format in .hg/requires but has no .git directory
26 26 [255]
27 27 $ git init
28 28 Initialized empty Git repository in $TESTTMP/nogit/.git/
29 29 This status invocation shows some hg gunk because we didn't use
30 30 `hg init --git`, which fixes up .git/info/exclude for us.
31 31 $ hg status
32 32 ? .hg/cache/git-commits.sqlite
33 33 ? .hg/cache/git-commits.sqlite-shm
34 34 ? .hg/cache/git-commits.sqlite-wal
35 35 ? .hg/requires
36 36 $ cd ..
37 37
38 38 Now globally enable extension for the rest of the test:
39 39 $ cat <<EOF >> $HGRCPATH
40 40 > [extensions]
41 41 > git=
42 42 > [git]
43 43 > log-index-cache-miss = yes
44 44 > EOF
45 45
46 46 Make a new repo with git:
47 47 $ mkdir foo
48 48 $ cd foo
49 49 $ git init
50 50 Initialized empty Git repository in $TESTTMP/foo/.git/
51 51 Ignore the .hg directory within git:
52 52 $ echo .hg >> .git/info/exclude
53 53 $ echo alpha > alpha
54 54 $ git add alpha
55 55 $ gitcommit -am 'Add alpha'
56 56 $ echo beta > beta
57 57 $ git add beta
58 58 $ gitcommit -am 'Add beta'
59 59 $ echo gamma > gamma
60 60 $ git status
61 61 On branch master
62 62 Untracked files:
63 63 (use "git add <file>..." to include in what will be committed)
64 64 gamma
65 65
66 66 nothing added to commit but untracked files present (use "git add" to track)
67 67
68 68 Without creating the .hg, hg status fails:
69 69 $ hg status
70 70 abort: no repository found in '$TESTTMP/foo' (.hg not found)!
71 71 [255]
72 72 But if you run hg init --git, it works:
73 73 $ hg init --git
74 74 $ hg id --traceback
75 75 heads mismatch, rebuilding dagcache
76 76 3d9be8deba43 tip master
77 77 $ hg status
78 78 ? gamma
79 79 Log works too:
80 80 $ hg log
81 81 changeset: 1:3d9be8deba43
82 82 bookmark: master
83 83 tag: tip
84 84 user: test <test@example.org>
85 85 date: Mon Jan 01 00:00:11 2007 +0000
86 86 summary: Add beta
87 87
88 88 changeset: 0:c5864c9d16fb
89 89 user: test <test@example.org>
90 90 date: Mon Jan 01 00:00:10 2007 +0000
91 91 summary: Add alpha
92 92
93 93
94 94
95 95 and bookmarks:
96 96 $ hg bookmarks
97 97 * master 1:3d9be8deba43
98 98
99 99 diff even works transparently in both systems:
100 100 $ echo blah >> alpha
101 101 $ git diff
102 102 diff --git a/alpha b/alpha
103 103 index 4a58007..faed1b7 100644
104 104 --- a/alpha
105 105 +++ b/alpha
106 106 @@ -1* +1,2 @@ (glob)
107 107 alpha
108 108 +blah
109 109 $ hg diff --git
110 110 diff --git a/alpha b/alpha
111 111 --- a/alpha
112 112 +++ b/alpha
113 113 @@ -1,1 +1,2 @@
114 114 alpha
115 115 +blah
116 116
117 117 Remove a file, it shows as such:
118 118 $ rm alpha
119 119 $ hg status
120 120 ! alpha
121 121 ? gamma
122 122
123 123 Revert works:
124 124 $ hg revert alpha --traceback
125 125 $ hg status
126 126 ? gamma
127 127 $ git status
128 128 On branch master
129 129 Untracked files:
130 130 (use "git add <file>..." to include in what will be committed)
131 131 gamma
132 132
133 133 nothing added to commit but untracked files present (use "git add" to track)
134 134
135 135 Add shows sanely in both:
136 136 $ hg add gamma
137 137 $ hg status
138 138 A gamma
139 139 $ hg files
140 140 alpha
141 141 beta
142 142 gamma
143 143 $ git ls-files
144 144 alpha
145 145 beta
146 146 gamma
147 147 $ git status
148 148 On branch master
149 149 Changes to be committed:
150 150 (use "git restore --staged <file>..." to unstage)
151 151 new file: gamma
152 152
153 153
154 154 forget does what it should as well:
155 155 $ hg forget gamma
156 156 $ hg status
157 157 ? gamma
158 158 $ git status
159 159 On branch master
160 160 Untracked files:
161 161 (use "git add <file>..." to include in what will be committed)
162 162 gamma
163 163
164 164 nothing added to commit but untracked files present (use "git add" to track)
165 165
166 166 clean up untracked file
167 167 $ rm gamma
168 168
169 169 hg log FILE
170 170
171 171 $ echo a >> alpha
172 172 $ hg ci -m 'more alpha' --traceback --date '1583522787 18000'
173 173 $ echo b >> beta
174 174 $ hg ci -m 'more beta'
175 175 heads mismatch, rebuilding dagcache
176 176 $ echo a >> alpha
177 177 $ hg ci -m 'even more alpha'
178 178 heads mismatch, rebuilding dagcache
179 179 $ hg log -G alpha
180 180 heads mismatch, rebuilding dagcache
181 181 @ changeset: 4:6626247b7dc8
182 182 : bookmark: master
183 183 : tag: tip
184 184 : user: test <test>
185 185 : date: Thu Jan 01 00:00:00 1970 +0000
186 186 : summary: even more alpha
187 187 :
188 188 o changeset: 2:a1983dd7fb19
189 189 : user: test <test>
190 190 : date: Fri Mar 06 14:26:27 2020 -0500
191 191 : summary: more alpha
192 192 :
193 193 o changeset: 0:c5864c9d16fb
194 194 user: test <test@example.org>
195 195 date: Mon Jan 01 00:00:10 2007 +0000
196 196 summary: Add alpha
197 197
198 198 $ hg log -G beta
199 199 o changeset: 3:d8ee22687733
200 200 : user: test <test>
201 201 : date: Thu Jan 01 00:00:00 1970 +0000
202 202 : summary: more beta
203 203 :
204 204 o changeset: 1:3d9be8deba43
205 205 | user: test <test@example.org>
206 206 ~ date: Mon Jan 01 00:00:11 2007 +0000
207 207 summary: Add beta
208 208
209 209
210 210 $ hg log -r "children(3d9be8deba43)" -T"{node|short} {children}\n"
211 211 a1983dd7fb19 3:d8ee22687733
212 212
213 213 hg annotate
214 214
215 215 $ hg annotate alpha
216 216 0: alpha
217 217 2: a
218 218 4: a
219 219 $ hg annotate beta
220 220 1: beta
221 221 3: b
222 222
223 223
224 224 Files in subdirectories. TODO: case-folding support, make this `A`
225 225 instead of `a`.
226 226
227 227 $ mkdir a
228 228 $ echo "This is file mu." > a/mu
229 229 $ hg ci -A -m 'Introduce file a/mu'
230 230 adding a/mu
231 231
232 232 Both hg and git agree a/mu is part of the repo
233 233
234 234 $ git ls-files
235 235 a/mu
236 236 alpha
237 237 beta
238 238 $ hg files
239 239 a/mu
240 240 alpha
241 241 beta
242 242
243 243 hg and git status both clean
244 244
245 245 $ git status
246 246 On branch master
247 247 nothing to commit, working tree clean
248 248 $ hg status
249 249 heads mismatch, rebuilding dagcache
250 250
251 251
252 252 node|shortest works correctly
253 253 $ hg log -T '{node}\n' | sort
254 254 3d9be8deba43482be2c81a4cb4be1f10d85fa8bc
255 255 6626247b7dc8f231b183b8a4761c89139baca2ad
256 256 a1983dd7fb19cbd83ad5a1c2fc8bf3d775dea12f
257 257 ae1ab744f95bfd5b07cf573baef98a778058537b
258 258 c5864c9d16fb3431fe2c175ff84dc6accdbb2c18
259 259 d8ee22687733a1991813560b15128cd9734f4b48
260 260 $ hg log -r ae1ab744f95bfd5b07cf573baef98a778058537b --template "{shortest(node,1)}\n"
261 261 ae
262 262
263 263 This coveres changelog.findmissing()
264 264 $ hg merge --preview 3d9be8deba43
265 265
266 266 This covers manifest.diff()
267 267 $ hg diff -c 3d9be8deba43
268 268 diff -r c5864c9d16fb -r 3d9be8deba43 beta
269 269 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
270 270 +++ b/beta Mon Jan 01 00:00:11 2007 +0000
271 271 @@ -0,0 +1,1 @@
272 272 +beta
273
274
275 Deleting files should also work (this was issue6398)
276 $ hg rm beta
277 $ hg ci -m 'remove beta'
General Comments 0
You need to be logged in to leave comments. Login now