##// END OF EJS Templates
git: fix index handling of removed files during commit (issue6398)...
Augie Fackler -
r45981:8e997c5d default draft
parent child Browse files
Show More
@@ -1,337 +1,339 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(modified + added + removed + deleted + unknown + ignored)
174 174 index = self.git.index
175 175 index.read()
176 176 for entry in index:
177 177 path = pycompat.fsencode(entry.path)
178 178 if not match(path):
179 179 continue
180 180 if path in observed:
181 181 continue # already in some other set
182 182 if path[-1] == b'/':
183 183 continue # directory
184 184 clean.append(path)
185 185
186 186 # TODO are we really always sure of status here?
187 187 return (
188 188 False,
189 189 scmutil.status(
190 190 modified, added, removed, deleted, unknown, ignored, clean
191 191 ),
192 192 )
193 193
194 194 def flagfunc(self, buildfallback):
195 195 # TODO we can do better
196 196 return buildfallback()
197 197
198 198 def getcwd(self):
199 199 # TODO is this a good way to do this?
200 200 return os.path.dirname(
201 201 os.path.dirname(pycompat.fsencode(self.git.path))
202 202 )
203 203
204 204 def normalize(self, path):
205 205 normed = util.normcase(path)
206 206 assert normed == path, b"TODO handling of case folding: %s != %s" % (
207 207 normed,
208 208 path,
209 209 )
210 210 return path
211 211
212 212 @property
213 213 def _checklink(self):
214 214 return util.checklink(os.path.dirname(pycompat.fsencode(self.git.path)))
215 215
216 216 def copies(self):
217 217 # TODO support copies?
218 218 return {}
219 219
220 220 # # TODO what the heck is this
221 221 _filecache = set()
222 222
223 223 def pendingparentchange(self):
224 224 # TODO: we need to implement the context manager bits and
225 225 # correctly stage/revert index edits.
226 226 return False
227 227
228 228 def write(self, tr):
229 229 # TODO: call parent change callbacks
230 230
231 231 if tr:
232 232
233 233 def writeinner(category):
234 234 self.git.index.write()
235 235
236 236 tr.addpending(b'gitdirstate', writeinner)
237 237 else:
238 238 self.git.index.write()
239 239
240 240 def pathto(self, f, cwd=None):
241 241 if cwd is None:
242 242 cwd = self.getcwd()
243 243 # TODO core dirstate does something about slashes here
244 244 assert isinstance(f, bytes)
245 245 r = util.pathto(self._root, cwd, f)
246 246 return r
247 247
248 248 def matches(self, match):
249 249 for x in self.git.index:
250 250 p = pycompat.fsencode(x.path)
251 251 if match(p):
252 252 yield p
253 253
254 254 def normal(self, f, parentfiledata=None):
255 255 """Mark a file normal and clean."""
256 256 # TODO: for now we just let libgit2 re-stat the file. We can
257 257 # clearly do better.
258 258
259 259 def normallookup(self, f):
260 260 """Mark a file normal, but possibly dirty."""
261 261 # TODO: for now we just let libgit2 re-stat the file. We can
262 262 # clearly do better.
263 263
264 264 def walk(self, match, subrepos, unknown, ignored, full=True):
265 265 # TODO: we need to use .status() and not iterate the index,
266 266 # because the index doesn't force a re-walk and so `hg add` of
267 267 # a new file without an intervening call to status will
268 268 # silently do nothing.
269 269 r = {}
270 270 cwd = self.getcwd()
271 271 for path, status in self.git.status().items():
272 272 if path.startswith('.hg/'):
273 273 continue
274 274 path = pycompat.fsencode(path)
275 275 if not match(path):
276 276 continue
277 277 # TODO construct the stat info from the status object?
278 278 try:
279 279 s = os.stat(os.path.join(cwd, path))
280 280 except OSError as e:
281 281 if e.errno != errno.ENOENT:
282 282 raise
283 283 continue
284 284 r[path] = s
285 285 return r
286 286
287 287 def savebackup(self, tr, backupname):
288 288 # TODO: figure out a strategy for saving index backups.
289 289 pass
290 290
291 291 def restorebackup(self, tr, backupname):
292 292 # TODO: figure out a strategy for saving index backups.
293 293 pass
294 294
295 295 def add(self, f):
296 296 index = self.git.index
297 297 index.read()
298 298 index.add(pycompat.fsdecode(f))
299 299 index.write()
300 300
301 301 def drop(self, f):
302 302 index = self.git.index
303 303 index.read()
304 index.remove(pycompat.fsdecode(f))
305 index.write()
304 fs = pycompat.fsdecode(f)
305 if fs in index:
306 index.remove(fs)
307 index.write()
306 308
307 309 def remove(self, f):
308 310 index = self.git.index
309 311 index.read()
310 312 index.remove(pycompat.fsdecode(f))
311 313 index.write()
312 314
313 315 def copied(self, path):
314 316 # TODO: track copies?
315 317 return None
316 318
317 319 def prefetch_parents(self):
318 320 # TODO
319 321 pass
320 322
321 323 @contextlib.contextmanager
322 324 def parentchange(self):
323 325 # TODO: track this maybe?
324 326 yield
325 327
326 328 def addparentchangecallback(self, category, callback):
327 329 # TODO: should this be added to the dirstate interface?
328 330 self._plchangecallbacks[category] = callback
329 331
330 332 def clearbackup(self, tr, backupname):
331 333 # TODO
332 334 pass
333 335
334 336 def setbranch(self, branch):
335 337 raise error.Abort(
336 338 b'git repos do not support branches. try using bookmarks'
337 339 )
@@ -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