##// END OF EJS Templates
merge with stable
Martin von Zweigbergk -
r38445:00368bc0 merge default
parent child Browse files
Show More
@@ -1,940 +1,952 b''
1 1 /*
2 2 * manifest.c - manifest type that does on-demand parsing.
3 3 *
4 4 * Copyright 2015, Google Inc.
5 5 *
6 6 * This software may be used and distributed according to the terms of
7 7 * the GNU General Public License, incorporated herein by reference.
8 8 */
9 9 #include <Python.h>
10 10
11 11 #include <assert.h>
12 12 #include <stdlib.h>
13 13 #include <string.h>
14 14
15 15 #include "charencode.h"
16 16 #include "util.h"
17 17
18 18 #define DEFAULT_LINES 100000
19 19
20 20 typedef struct {
21 21 char *start;
22 22 Py_ssize_t len; /* length of line including terminal newline */
23 23 char hash_suffix;
24 24 bool from_malloc;
25 25 bool deleted;
26 26 } line;
27 27
28 28 typedef struct {
29 29 PyObject_HEAD
30 30 PyObject *pydata;
31 31 line *lines;
32 32 int numlines; /* number of line entries */
33 33 int livelines; /* number of non-deleted lines */
34 34 int maxlines; /* allocated number of lines */
35 35 bool dirty;
36 36 } lazymanifest;
37 37
38 38 #define MANIFEST_OOM -1
39 39 #define MANIFEST_NOT_SORTED -2
40 40 #define MANIFEST_MALFORMED -3
41 41
42 42 /* get the length of the path for a line */
43 43 static size_t pathlen(line *l)
44 44 {
45 45 return strlen(l->start);
46 46 }
47 47
48 48 /* get the node value of a single line */
49 49 static PyObject *nodeof(line *l)
50 50 {
51 51 char *s = l->start;
52 52 ssize_t llen = pathlen(l);
53 53 PyObject *hash = unhexlify(s + llen + 1, 40);
54 54 if (!hash) {
55 55 return NULL;
56 56 }
57 57 if (l->hash_suffix != '\0') {
58 58 char newhash[21];
59 59 memcpy(newhash, PyBytes_AsString(hash), 20);
60 60 Py_DECREF(hash);
61 61 newhash[20] = l->hash_suffix;
62 62 hash = PyBytes_FromStringAndSize(newhash, 21);
63 63 }
64 64 return hash;
65 65 }
66 66
67 67 /* get the node hash and flags of a line as a tuple */
68 68 static PyObject *hashflags(line *l)
69 69 {
70 70 char *s = l->start;
71 71 size_t plen = pathlen(l);
72 72 PyObject *hash = nodeof(l);
73 73
74 74 /* 40 for hash, 1 for null byte, 1 for newline */
75 75 size_t hplen = plen + 42;
76 76 Py_ssize_t flen = l->len - hplen;
77 77 PyObject *flags;
78 78 PyObject *tup;
79 79
80 80 if (!hash)
81 81 return NULL;
82 82 flags = PyBytes_FromStringAndSize(s + hplen - 1, flen);
83 83 if (!flags) {
84 84 Py_DECREF(hash);
85 85 return NULL;
86 86 }
87 87 tup = PyTuple_Pack(2, hash, flags);
88 88 Py_DECREF(flags);
89 89 Py_DECREF(hash);
90 90 return tup;
91 91 }
92 92
93 93 /* if we're about to run out of space in the line index, add more */
94 94 static bool realloc_if_full(lazymanifest *self)
95 95 {
96 96 if (self->numlines == self->maxlines) {
97 97 self->maxlines *= 2;
98 98 self->lines = realloc(self->lines, self->maxlines * sizeof(line));
99 99 }
100 100 return !!self->lines;
101 101 }
102 102
103 103 /*
104 104 * Find the line boundaries in the manifest that 'data' points to and store
105 105 * information about each line in 'self'.
106 106 */
107 107 static int find_lines(lazymanifest *self, char *data, Py_ssize_t len)
108 108 {
109 109 char *prev = NULL;
110 110 while (len > 0) {
111 111 line *l;
112 112 char *next = memchr(data, '\n', len);
113 113 if (!next) {
114 114 return MANIFEST_MALFORMED;
115 115 }
116 116 next++; /* advance past newline */
117 117 if (!realloc_if_full(self)) {
118 118 return MANIFEST_OOM; /* no memory */
119 119 }
120 120 if (prev && strcmp(prev, data) > -1) {
121 121 /* This data isn't sorted, so we have to abort. */
122 122 return MANIFEST_NOT_SORTED;
123 123 }
124 124 l = self->lines + ((self->numlines)++);
125 125 l->start = data;
126 126 l->len = next - data;
127 127 l->hash_suffix = '\0';
128 128 l->from_malloc = false;
129 129 l->deleted = false;
130 130 len = len - l->len;
131 131 prev = data;
132 132 data = next;
133 133 }
134 134 self->livelines = self->numlines;
135 135 return 0;
136 136 }
137 137
138 static void lazymanifest_init_early(lazymanifest *self)
139 {
140 self->pydata = NULL;
141 self->lines = NULL;
142 self->numlines = 0;
143 self->maxlines = 0;
144 }
145
138 146 static int lazymanifest_init(lazymanifest *self, PyObject *args)
139 147 {
140 148 char *data;
141 149 Py_ssize_t len;
142 150 int err, ret;
143 151 PyObject *pydata;
152
153 lazymanifest_init_early(self);
144 154 if (!PyArg_ParseTuple(args, "S", &pydata)) {
145 155 return -1;
146 156 }
147 157 err = PyBytes_AsStringAndSize(pydata, &data, &len);
148 158
149 159 self->dirty = false;
150 160 if (err == -1)
151 161 return -1;
152 162 self->pydata = pydata;
153 163 Py_INCREF(self->pydata);
154 164 Py_BEGIN_ALLOW_THREADS
155 165 self->lines = malloc(DEFAULT_LINES * sizeof(line));
156 166 self->maxlines = DEFAULT_LINES;
157 167 self->numlines = 0;
158 168 if (!self->lines)
159 169 ret = MANIFEST_OOM;
160 170 else
161 171 ret = find_lines(self, data, len);
162 172 Py_END_ALLOW_THREADS
163 173 switch (ret) {
164 174 case 0:
165 175 break;
166 176 case MANIFEST_OOM:
167 177 PyErr_NoMemory();
168 178 break;
169 179 case MANIFEST_NOT_SORTED:
170 180 PyErr_Format(PyExc_ValueError,
171 181 "Manifest lines not in sorted order.");
172 182 break;
173 183 case MANIFEST_MALFORMED:
174 184 PyErr_Format(PyExc_ValueError,
175 185 "Manifest did not end in a newline.");
176 186 break;
177 187 default:
178 188 PyErr_Format(PyExc_ValueError,
179 189 "Unknown problem parsing manifest.");
180 190 }
181 191 return ret == 0 ? 0 : -1;
182 192 }
183 193
184 194 static void lazymanifest_dealloc(lazymanifest *self)
185 195 {
186 196 /* free any extra lines we had to allocate */
187 197 int i;
188 198 for (i = 0; self->lines && (i < self->numlines); i++) {
189 199 if (self->lines[i].from_malloc) {
190 200 free(self->lines[i].start);
191 201 }
192 202 }
193 203 free(self->lines);
194 204 self->lines = NULL;
195 205 if (self->pydata) {
196 206 Py_DECREF(self->pydata);
197 207 self->pydata = NULL;
198 208 }
199 209 PyObject_Del(self);
200 210 }
201 211
202 212 /* iteration support */
203 213
204 214 typedef struct {
205 215 PyObject_HEAD lazymanifest *m;
206 216 Py_ssize_t pos;
207 217 } lmIter;
208 218
209 219 static void lmiter_dealloc(PyObject *o)
210 220 {
211 221 lmIter *self = (lmIter *)o;
212 222 Py_DECREF(self->m);
213 223 PyObject_Del(self);
214 224 }
215 225
216 226 static line *lmiter_nextline(lmIter *self)
217 227 {
218 228 do {
219 229 self->pos++;
220 230 if (self->pos >= self->m->numlines) {
221 231 return NULL;
222 232 }
223 233 /* skip over deleted manifest entries */
224 234 } while (self->m->lines[self->pos].deleted);
225 235 return self->m->lines + self->pos;
226 236 }
227 237
228 238 static PyObject *lmiter_iterentriesnext(PyObject *o)
229 239 {
230 240 size_t pl;
231 241 line *l;
232 242 Py_ssize_t consumed;
233 243 PyObject *ret = NULL, *path = NULL, *hash = NULL, *flags = NULL;
234 244 l = lmiter_nextline((lmIter *)o);
235 245 if (!l) {
236 246 goto done;
237 247 }
238 248 pl = pathlen(l);
239 249 path = PyBytes_FromStringAndSize(l->start, pl);
240 250 hash = nodeof(l);
241 251 consumed = pl + 41;
242 252 flags = PyBytes_FromStringAndSize(l->start + consumed,
243 253 l->len - consumed - 1);
244 254 if (!path || !hash || !flags) {
245 255 goto done;
246 256 }
247 257 ret = PyTuple_Pack(3, path, hash, flags);
248 258 done:
249 259 Py_XDECREF(path);
250 260 Py_XDECREF(hash);
251 261 Py_XDECREF(flags);
252 262 return ret;
253 263 }
254 264
255 265 #ifdef IS_PY3K
256 266 #define LAZYMANIFESTENTRIESITERATOR_TPFLAGS Py_TPFLAGS_DEFAULT
257 267 #else
258 268 #define LAZYMANIFESTENTRIESITERATOR_TPFLAGS Py_TPFLAGS_DEFAULT \
259 269 | Py_TPFLAGS_HAVE_ITER
260 270 #endif
261 271
262 272 static PyTypeObject lazymanifestEntriesIterator = {
263 273 PyVarObject_HEAD_INIT(NULL, 0) /* header */
264 274 "parsers.lazymanifest.entriesiterator", /*tp_name */
265 275 sizeof(lmIter), /*tp_basicsize */
266 276 0, /*tp_itemsize */
267 277 lmiter_dealloc, /*tp_dealloc */
268 278 0, /*tp_print */
269 279 0, /*tp_getattr */
270 280 0, /*tp_setattr */
271 281 0, /*tp_compare */
272 282 0, /*tp_repr */
273 283 0, /*tp_as_number */
274 284 0, /*tp_as_sequence */
275 285 0, /*tp_as_mapping */
276 286 0, /*tp_hash */
277 287 0, /*tp_call */
278 288 0, /*tp_str */
279 289 0, /*tp_getattro */
280 290 0, /*tp_setattro */
281 291 0, /*tp_as_buffer */
282 292 LAZYMANIFESTENTRIESITERATOR_TPFLAGS, /* tp_flags */
283 293 "Iterator for 3-tuples in a lazymanifest.", /* tp_doc */
284 294 0, /* tp_traverse */
285 295 0, /* tp_clear */
286 296 0, /* tp_richcompare */
287 297 0, /* tp_weaklistoffset */
288 298 PyObject_SelfIter, /* tp_iter: __iter__() method */
289 299 lmiter_iterentriesnext, /* tp_iternext: next() method */
290 300 };
291 301
292 302 static PyObject *lmiter_iterkeysnext(PyObject *o)
293 303 {
294 304 size_t pl;
295 305 line *l = lmiter_nextline((lmIter *)o);
296 306 if (!l) {
297 307 return NULL;
298 308 }
299 309 pl = pathlen(l);
300 310 return PyBytes_FromStringAndSize(l->start, pl);
301 311 }
302 312
303 313 #ifdef IS_PY3K
304 314 #define LAZYMANIFESTKEYSITERATOR_TPFLAGS Py_TPFLAGS_DEFAULT
305 315 #else
306 316 #define LAZYMANIFESTKEYSITERATOR_TPFLAGS Py_TPFLAGS_DEFAULT \
307 317 | Py_TPFLAGS_HAVE_ITER
308 318 #endif
309 319
310 320 static PyTypeObject lazymanifestKeysIterator = {
311 321 PyVarObject_HEAD_INIT(NULL, 0) /* header */
312 322 "parsers.lazymanifest.keysiterator", /*tp_name */
313 323 sizeof(lmIter), /*tp_basicsize */
314 324 0, /*tp_itemsize */
315 325 lmiter_dealloc, /*tp_dealloc */
316 326 0, /*tp_print */
317 327 0, /*tp_getattr */
318 328 0, /*tp_setattr */
319 329 0, /*tp_compare */
320 330 0, /*tp_repr */
321 331 0, /*tp_as_number */
322 332 0, /*tp_as_sequence */
323 333 0, /*tp_as_mapping */
324 334 0, /*tp_hash */
325 335 0, /*tp_call */
326 336 0, /*tp_str */
327 337 0, /*tp_getattro */
328 338 0, /*tp_setattro */
329 339 0, /*tp_as_buffer */
330 340 LAZYMANIFESTKEYSITERATOR_TPFLAGS, /* tp_flags */
331 341 "Keys iterator for a lazymanifest.", /* tp_doc */
332 342 0, /* tp_traverse */
333 343 0, /* tp_clear */
334 344 0, /* tp_richcompare */
335 345 0, /* tp_weaklistoffset */
336 346 PyObject_SelfIter, /* tp_iter: __iter__() method */
337 347 lmiter_iterkeysnext, /* tp_iternext: next() method */
338 348 };
339 349
340 350 static lazymanifest *lazymanifest_copy(lazymanifest *self);
341 351
342 352 static PyObject *lazymanifest_getentriesiter(lazymanifest *self)
343 353 {
344 354 lmIter *i = NULL;
345 355 lazymanifest *t = lazymanifest_copy(self);
346 356 if (!t) {
347 357 PyErr_NoMemory();
348 358 return NULL;
349 359 }
350 360 i = PyObject_New(lmIter, &lazymanifestEntriesIterator);
351 361 if (i) {
352 362 i->m = t;
353 363 i->pos = -1;
354 364 } else {
355 365 Py_DECREF(t);
356 366 PyErr_NoMemory();
357 367 }
358 368 return (PyObject *)i;
359 369 }
360 370
361 371 static PyObject *lazymanifest_getkeysiter(lazymanifest *self)
362 372 {
363 373 lmIter *i = NULL;
364 374 lazymanifest *t = lazymanifest_copy(self);
365 375 if (!t) {
366 376 PyErr_NoMemory();
367 377 return NULL;
368 378 }
369 379 i = PyObject_New(lmIter, &lazymanifestKeysIterator);
370 380 if (i) {
371 381 i->m = t;
372 382 i->pos = -1;
373 383 } else {
374 384 Py_DECREF(t);
375 385 PyErr_NoMemory();
376 386 }
377 387 return (PyObject *)i;
378 388 }
379 389
380 390 /* __getitem__ and __setitem__ support */
381 391
382 392 static Py_ssize_t lazymanifest_size(lazymanifest *self)
383 393 {
384 394 return self->livelines;
385 395 }
386 396
387 397 static int linecmp(const void *left, const void *right)
388 398 {
389 399 return strcmp(((const line *)left)->start,
390 400 ((const line *)right)->start);
391 401 }
392 402
393 403 static PyObject *lazymanifest_getitem(lazymanifest *self, PyObject *key)
394 404 {
395 405 line needle;
396 406 line *hit;
397 407 if (!PyBytes_Check(key)) {
398 408 PyErr_Format(PyExc_TypeError,
399 409 "getitem: manifest keys must be a string.");
400 410 return NULL;
401 411 }
402 412 needle.start = PyBytes_AsString(key);
403 413 hit = bsearch(&needle, self->lines, self->numlines, sizeof(line),
404 414 &linecmp);
405 415 if (!hit || hit->deleted) {
406 416 PyErr_Format(PyExc_KeyError, "No such manifest entry.");
407 417 return NULL;
408 418 }
409 419 return hashflags(hit);
410 420 }
411 421
412 422 static int lazymanifest_delitem(lazymanifest *self, PyObject *key)
413 423 {
414 424 line needle;
415 425 line *hit;
416 426 if (!PyBytes_Check(key)) {
417 427 PyErr_Format(PyExc_TypeError,
418 428 "delitem: manifest keys must be a string.");
419 429 return -1;
420 430 }
421 431 needle.start = PyBytes_AsString(key);
422 432 hit = bsearch(&needle, self->lines, self->numlines, sizeof(line),
423 433 &linecmp);
424 434 if (!hit || hit->deleted) {
425 435 PyErr_Format(PyExc_KeyError,
426 436 "Tried to delete nonexistent manifest entry.");
427 437 return -1;
428 438 }
429 439 self->dirty = true;
430 440 hit->deleted = true;
431 441 self->livelines--;
432 442 return 0;
433 443 }
434 444
435 445 /* Do a binary search for the insertion point for new, creating the
436 446 * new entry if needed. */
437 447 static int internalsetitem(lazymanifest *self, line *new)
438 448 {
439 449 int start = 0, end = self->numlines;
440 450 while (start < end) {
441 451 int pos = start + (end - start) / 2;
442 452 int c = linecmp(new, self->lines + pos);
443 453 if (c < 0)
444 454 end = pos;
445 455 else if (c > 0)
446 456 start = pos + 1;
447 457 else {
448 458 if (self->lines[pos].deleted)
449 459 self->livelines++;
450 460 if (self->lines[pos].from_malloc)
451 461 free(self->lines[pos].start);
452 462 start = pos;
453 463 goto finish;
454 464 }
455 465 }
456 466 /* being here means we need to do an insert */
457 467 if (!realloc_if_full(self)) {
458 468 PyErr_NoMemory();
459 469 return -1;
460 470 }
461 471 memmove(self->lines + start + 1, self->lines + start,
462 472 (self->numlines - start) * sizeof(line));
463 473 self->numlines++;
464 474 self->livelines++;
465 475 finish:
466 476 self->lines[start] = *new;
467 477 self->dirty = true;
468 478 return 0;
469 479 }
470 480
471 481 static int lazymanifest_setitem(
472 482 lazymanifest *self, PyObject *key, PyObject *value)
473 483 {
474 484 char *path;
475 485 Py_ssize_t plen;
476 486 PyObject *pyhash;
477 487 Py_ssize_t hlen;
478 488 char *hash;
479 489 PyObject *pyflags;
480 490 char *flags;
481 491 Py_ssize_t flen;
482 492 size_t dlen;
483 493 char *dest;
484 494 int i;
485 495 line new;
486 496 if (!PyBytes_Check(key)) {
487 497 PyErr_Format(PyExc_TypeError,
488 498 "setitem: manifest keys must be a string.");
489 499 return -1;
490 500 }
491 501 if (!value) {
492 502 return lazymanifest_delitem(self, key);
493 503 }
494 504 if (!PyTuple_Check(value) || PyTuple_Size(value) != 2) {
495 505 PyErr_Format(PyExc_TypeError,
496 506 "Manifest values must be a tuple of (node, flags).");
497 507 return -1;
498 508 }
499 509 if (PyBytes_AsStringAndSize(key, &path, &plen) == -1) {
500 510 return -1;
501 511 }
502 512
503 513 pyhash = PyTuple_GetItem(value, 0);
504 514 if (!PyBytes_Check(pyhash)) {
505 515 PyErr_Format(PyExc_TypeError,
506 516 "node must be a 20-byte string");
507 517 return -1;
508 518 }
509 519 hlen = PyBytes_Size(pyhash);
510 520 /* Some parts of the codebase try and set 21 or 22
511 521 * byte "hash" values in order to perturb things for
512 522 * status. We have to preserve at least the 21st
513 523 * byte. Sigh. If there's a 22nd byte, we drop it on
514 524 * the floor, which works fine.
515 525 */
516 526 if (hlen != 20 && hlen != 21 && hlen != 22) {
517 527 PyErr_Format(PyExc_TypeError,
518 528 "node must be a 20-byte string");
519 529 return -1;
520 530 }
521 531 hash = PyBytes_AsString(pyhash);
522 532
523 533 pyflags = PyTuple_GetItem(value, 1);
524 534 if (!PyBytes_Check(pyflags) || PyBytes_Size(pyflags) > 1) {
525 535 PyErr_Format(PyExc_TypeError,
526 536 "flags must a 0 or 1 byte string");
527 537 return -1;
528 538 }
529 539 if (PyBytes_AsStringAndSize(pyflags, &flags, &flen) == -1) {
530 540 return -1;
531 541 }
532 542 /* one null byte and one newline */
533 543 dlen = plen + 41 + flen + 1;
534 544 dest = malloc(dlen);
535 545 if (!dest) {
536 546 PyErr_NoMemory();
537 547 return -1;
538 548 }
539 549 memcpy(dest, path, plen + 1);
540 550 for (i = 0; i < 20; i++) {
541 551 /* Cast to unsigned, so it will not get sign-extended when promoted
542 552 * to int (as is done when passing to a variadic function)
543 553 */
544 554 sprintf(dest + plen + 1 + (i * 2), "%02x", (unsigned char)hash[i]);
545 555 }
546 556 memcpy(dest + plen + 41, flags, flen);
547 557 dest[plen + 41 + flen] = '\n';
548 558 new.start = dest;
549 559 new.len = dlen;
550 560 new.hash_suffix = '\0';
551 561 if (hlen > 20) {
552 562 new.hash_suffix = hash[20];
553 563 }
554 564 new.from_malloc = true; /* is `start` a pointer we allocated? */
555 565 new.deleted = false; /* is this entry deleted? */
556 566 if (internalsetitem(self, &new)) {
557 567 return -1;
558 568 }
559 569 return 0;
560 570 }
561 571
562 572 static PyMappingMethods lazymanifest_mapping_methods = {
563 573 (lenfunc)lazymanifest_size, /* mp_length */
564 574 (binaryfunc)lazymanifest_getitem, /* mp_subscript */
565 575 (objobjargproc)lazymanifest_setitem, /* mp_ass_subscript */
566 576 };
567 577
568 578 /* sequence methods (important or __contains__ builds an iterator) */
569 579
570 580 static int lazymanifest_contains(lazymanifest *self, PyObject *key)
571 581 {
572 582 line needle;
573 583 line *hit;
574 584 if (!PyBytes_Check(key)) {
575 585 /* Our keys are always strings, so if the contains
576 586 * check is for a non-string, just return false. */
577 587 return 0;
578 588 }
579 589 needle.start = PyBytes_AsString(key);
580 590 hit = bsearch(&needle, self->lines, self->numlines, sizeof(line),
581 591 &linecmp);
582 592 if (!hit || hit->deleted) {
583 593 return 0;
584 594 }
585 595 return 1;
586 596 }
587 597
588 598 static PySequenceMethods lazymanifest_seq_meths = {
589 599 (lenfunc)lazymanifest_size, /* sq_length */
590 600 0, /* sq_concat */
591 601 0, /* sq_repeat */
592 602 0, /* sq_item */
593 603 0, /* sq_slice */
594 604 0, /* sq_ass_item */
595 605 0, /* sq_ass_slice */
596 606 (objobjproc)lazymanifest_contains, /* sq_contains */
597 607 0, /* sq_inplace_concat */
598 608 0, /* sq_inplace_repeat */
599 609 };
600 610
601 611
602 612 /* Other methods (copy, diff, etc) */
603 613 static PyTypeObject lazymanifestType;
604 614
605 615 /* If the manifest has changes, build the new manifest text and reindex it. */
606 616 static int compact(lazymanifest *self)
607 617 {
608 618 int i;
609 619 ssize_t need = 0;
610 620 char *data;
611 621 line *src, *dst;
612 622 PyObject *pydata;
613 623 if (!self->dirty)
614 624 return 0;
615 625 for (i = 0; i < self->numlines; i++) {
616 626 if (!self->lines[i].deleted) {
617 627 need += self->lines[i].len;
618 628 }
619 629 }
620 630 pydata = PyBytes_FromStringAndSize(NULL, need);
621 631 if (!pydata)
622 632 return -1;
623 633 data = PyBytes_AsString(pydata);
624 634 if (!data) {
625 635 return -1;
626 636 }
627 637 src = self->lines;
628 638 dst = self->lines;
629 639 for (i = 0; i < self->numlines; i++, src++) {
630 640 char *tofree = NULL;
631 641 if (src->from_malloc) {
632 642 tofree = src->start;
633 643 }
634 644 if (!src->deleted) {
635 645 memcpy(data, src->start, src->len);
636 646 *dst = *src;
637 647 dst->start = data;
638 648 dst->from_malloc = false;
639 649 data += dst->len;
640 650 dst++;
641 651 }
642 652 free(tofree);
643 653 }
644 654 Py_DECREF(self->pydata);
645 655 self->pydata = pydata;
646 656 self->numlines = self->livelines;
647 657 self->dirty = false;
648 658 return 0;
649 659 }
650 660
651 661 static PyObject *lazymanifest_text(lazymanifest *self)
652 662 {
653 663 if (compact(self) != 0) {
654 664 PyErr_NoMemory();
655 665 return NULL;
656 666 }
657 667 Py_INCREF(self->pydata);
658 668 return self->pydata;
659 669 }
660 670
661 671 static lazymanifest *lazymanifest_copy(lazymanifest *self)
662 672 {
663 673 lazymanifest *copy = NULL;
664 674 if (compact(self) != 0) {
665 675 goto nomem;
666 676 }
667 677 copy = PyObject_New(lazymanifest, &lazymanifestType);
668 678 if (!copy) {
669 679 goto nomem;
670 680 }
681 lazymanifest_init_early(copy);
671 682 copy->numlines = self->numlines;
672 683 copy->livelines = self->livelines;
673 684 copy->dirty = false;
674 685 copy->lines = malloc(self->maxlines *sizeof(line));
675 686 if (!copy->lines) {
676 687 goto nomem;
677 688 }
678 689 memcpy(copy->lines, self->lines, self->numlines * sizeof(line));
679 690 copy->maxlines = self->maxlines;
680 691 copy->pydata = self->pydata;
681 692 Py_INCREF(copy->pydata);
682 693 return copy;
683 694 nomem:
684 695 PyErr_NoMemory();
685 696 Py_XDECREF(copy);
686 697 return NULL;
687 698 }
688 699
689 700 static lazymanifest *lazymanifest_filtercopy(
690 701 lazymanifest *self, PyObject *matchfn)
691 702 {
692 703 lazymanifest *copy = NULL;
693 704 int i;
694 705 if (!PyCallable_Check(matchfn)) {
695 706 PyErr_SetString(PyExc_TypeError, "matchfn must be callable");
696 707 return NULL;
697 708 }
698 709 /* compact ourselves first to avoid double-frees later when we
699 710 * compact tmp so that it doesn't have random pointers to our
700 711 * underlying from_malloc-data (self->pydata is safe) */
701 712 if (compact(self) != 0) {
702 713 goto nomem;
703 714 }
704 715 copy = PyObject_New(lazymanifest, &lazymanifestType);
705 716 if (!copy) {
706 717 goto nomem;
707 718 }
719 lazymanifest_init_early(copy);
708 720 copy->dirty = true;
709 721 copy->lines = malloc(self->maxlines * sizeof(line));
710 722 if (!copy->lines) {
711 723 goto nomem;
712 724 }
713 725 copy->maxlines = self->maxlines;
714 726 copy->numlines = 0;
715 727 copy->pydata = self->pydata;
716 728 Py_INCREF(self->pydata);
717 729 for (i = 0; i < self->numlines; i++) {
718 730 PyObject *arglist = NULL, *result = NULL;
719 731 arglist = Py_BuildValue(PY23("(s)", "(y)"),
720 732 self->lines[i].start);
721 733 if (!arglist) {
722 734 return NULL;
723 735 }
724 736 result = PyObject_CallObject(matchfn, arglist);
725 737 Py_DECREF(arglist);
726 738 /* if the callback raised an exception, just let it
727 739 * through and give up */
728 740 if (!result) {
729 741 free(copy->lines);
730 742 Py_DECREF(self->pydata);
731 743 return NULL;
732 744 }
733 745 if (PyObject_IsTrue(result)) {
734 746 assert(!(self->lines[i].from_malloc));
735 747 copy->lines[copy->numlines++] = self->lines[i];
736 748 }
737 749 Py_DECREF(result);
738 750 }
739 751 copy->livelines = copy->numlines;
740 752 return copy;
741 753 nomem:
742 754 PyErr_NoMemory();
743 755 Py_XDECREF(copy);
744 756 return NULL;
745 757 }
746 758
747 759 static PyObject *lazymanifest_diff(lazymanifest *self, PyObject *args)
748 760 {
749 761 lazymanifest *other;
750 762 PyObject *pyclean = NULL;
751 763 bool listclean;
752 764 PyObject *emptyTup = NULL, *ret = NULL;
753 765 PyObject *es;
754 766 int sneedle = 0, oneedle = 0;
755 767 if (!PyArg_ParseTuple(args, "O!|O", &lazymanifestType, &other, &pyclean)) {
756 768 return NULL;
757 769 }
758 770 listclean = (!pyclean) ? false : PyObject_IsTrue(pyclean);
759 771 es = PyBytes_FromString("");
760 772 if (!es) {
761 773 goto nomem;
762 774 }
763 775 emptyTup = PyTuple_Pack(2, Py_None, es);
764 776 Py_DECREF(es);
765 777 if (!emptyTup) {
766 778 goto nomem;
767 779 }
768 780 ret = PyDict_New();
769 781 if (!ret) {
770 782 goto nomem;
771 783 }
772 784 while (sneedle != self->numlines || oneedle != other->numlines) {
773 785 line *left = self->lines + sneedle;
774 786 line *right = other->lines + oneedle;
775 787 int result;
776 788 PyObject *key;
777 789 PyObject *outer;
778 790 /* If we're looking at a deleted entry and it's not
779 791 * the end of the manifest, just skip it. */
780 792 if (sneedle < self->numlines && left->deleted) {
781 793 sneedle++;
782 794 continue;
783 795 }
784 796 if (oneedle < other->numlines && right->deleted) {
785 797 oneedle++;
786 798 continue;
787 799 }
788 800 /* if we're at the end of either manifest, then we
789 801 * know the remaining items are adds so we can skip
790 802 * the strcmp. */
791 803 if (sneedle == self->numlines) {
792 804 result = 1;
793 805 } else if (oneedle == other->numlines) {
794 806 result = -1;
795 807 } else {
796 808 result = linecmp(left, right);
797 809 }
798 810 key = result <= 0 ?
799 811 PyBytes_FromString(left->start) :
800 812 PyBytes_FromString(right->start);
801 813 if (!key)
802 814 goto nomem;
803 815 if (result < 0) {
804 816 PyObject *l = hashflags(left);
805 817 if (!l) {
806 818 goto nomem;
807 819 }
808 820 outer = PyTuple_Pack(2, l, emptyTup);
809 821 Py_DECREF(l);
810 822 if (!outer) {
811 823 goto nomem;
812 824 }
813 825 PyDict_SetItem(ret, key, outer);
814 826 Py_DECREF(outer);
815 827 sneedle++;
816 828 } else if (result > 0) {
817 829 PyObject *r = hashflags(right);
818 830 if (!r) {
819 831 goto nomem;
820 832 }
821 833 outer = PyTuple_Pack(2, emptyTup, r);
822 834 Py_DECREF(r);
823 835 if (!outer) {
824 836 goto nomem;
825 837 }
826 838 PyDict_SetItem(ret, key, outer);
827 839 Py_DECREF(outer);
828 840 oneedle++;
829 841 } else {
830 842 /* file exists in both manifests */
831 843 if (left->len != right->len
832 844 || memcmp(left->start, right->start, left->len)
833 845 || left->hash_suffix != right->hash_suffix) {
834 846 PyObject *l = hashflags(left);
835 847 PyObject *r;
836 848 if (!l) {
837 849 goto nomem;
838 850 }
839 851 r = hashflags(right);
840 852 if (!r) {
841 853 Py_DECREF(l);
842 854 goto nomem;
843 855 }
844 856 outer = PyTuple_Pack(2, l, r);
845 857 Py_DECREF(l);
846 858 Py_DECREF(r);
847 859 if (!outer) {
848 860 goto nomem;
849 861 }
850 862 PyDict_SetItem(ret, key, outer);
851 863 Py_DECREF(outer);
852 864 } else if (listclean) {
853 865 PyDict_SetItem(ret, key, Py_None);
854 866 }
855 867 sneedle++;
856 868 oneedle++;
857 869 }
858 870 Py_DECREF(key);
859 871 }
860 872 Py_DECREF(emptyTup);
861 873 return ret;
862 874 nomem:
863 875 PyErr_NoMemory();
864 876 Py_XDECREF(ret);
865 877 Py_XDECREF(emptyTup);
866 878 return NULL;
867 879 }
868 880
869 881 static PyMethodDef lazymanifest_methods[] = {
870 882 {"iterkeys", (PyCFunction)lazymanifest_getkeysiter, METH_NOARGS,
871 883 "Iterate over file names in this lazymanifest."},
872 884 {"iterentries", (PyCFunction)lazymanifest_getentriesiter, METH_NOARGS,
873 885 "Iterate over (path, nodeid, flags) tuples in this lazymanifest."},
874 886 {"copy", (PyCFunction)lazymanifest_copy, METH_NOARGS,
875 887 "Make a copy of this lazymanifest."},
876 888 {"filtercopy", (PyCFunction)lazymanifest_filtercopy, METH_O,
877 889 "Make a copy of this manifest filtered by matchfn."},
878 890 {"diff", (PyCFunction)lazymanifest_diff, METH_VARARGS,
879 891 "Compare this lazymanifest to another one."},
880 892 {"text", (PyCFunction)lazymanifest_text, METH_NOARGS,
881 893 "Encode this manifest to text."},
882 894 {NULL},
883 895 };
884 896
885 897 #ifdef IS_PY3K
886 898 #define LAZYMANIFEST_TPFLAGS Py_TPFLAGS_DEFAULT
887 899 #else
888 900 #define LAZYMANIFEST_TPFLAGS Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_SEQUENCE_IN
889 901 #endif
890 902
891 903 static PyTypeObject lazymanifestType = {
892 904 PyVarObject_HEAD_INIT(NULL, 0) /* header */
893 905 "parsers.lazymanifest", /* tp_name */
894 906 sizeof(lazymanifest), /* tp_basicsize */
895 907 0, /* tp_itemsize */
896 908 (destructor)lazymanifest_dealloc, /* tp_dealloc */
897 909 0, /* tp_print */
898 910 0, /* tp_getattr */
899 911 0, /* tp_setattr */
900 912 0, /* tp_compare */
901 913 0, /* tp_repr */
902 914 0, /* tp_as_number */
903 915 &lazymanifest_seq_meths, /* tp_as_sequence */
904 916 &lazymanifest_mapping_methods, /* tp_as_mapping */
905 917 0, /* tp_hash */
906 918 0, /* tp_call */
907 919 0, /* tp_str */
908 920 0, /* tp_getattro */
909 921 0, /* tp_setattro */
910 922 0, /* tp_as_buffer */
911 923 LAZYMANIFEST_TPFLAGS, /* tp_flags */
912 924 "TODO(augie)", /* tp_doc */
913 925 0, /* tp_traverse */
914 926 0, /* tp_clear */
915 927 0, /* tp_richcompare */
916 928 0, /* tp_weaklistoffset */
917 929 (getiterfunc)lazymanifest_getkeysiter, /* tp_iter */
918 930 0, /* tp_iternext */
919 931 lazymanifest_methods, /* tp_methods */
920 932 0, /* tp_members */
921 933 0, /* tp_getset */
922 934 0, /* tp_base */
923 935 0, /* tp_dict */
924 936 0, /* tp_descr_get */
925 937 0, /* tp_descr_set */
926 938 0, /* tp_dictoffset */
927 939 (initproc)lazymanifest_init, /* tp_init */
928 940 0, /* tp_alloc */
929 941 };
930 942
931 943 void manifest_module_init(PyObject * mod)
932 944 {
933 945 lazymanifestType.tp_new = PyType_GenericNew;
934 946 if (PyType_Ready(&lazymanifestType) < 0)
935 947 return;
936 948 Py_INCREF(&lazymanifestType);
937 949
938 950 PyModule_AddObject(mod, "lazymanifest",
939 951 (PyObject *)&lazymanifestType);
940 952 }
@@ -1,1784 +1,1784 b''
1 1 # stuff related specifically to patch manipulation / parsing
2 2 #
3 3 # Copyright 2008 Mark Edgington <edgimar@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7 #
8 8 # This code is based on the Mark Edgington's crecord extension.
9 9 # (Itself based on Bryan O'Sullivan's record extension.)
10 10
11 11 from __future__ import absolute_import
12 12
13 13 import locale
14 14 import os
15 15 import re
16 16 import signal
17 17
18 18 from .i18n import _
19 19 from . import (
20 20 encoding,
21 21 error,
22 22 patch as patchmod,
23 23 scmutil,
24 24 util,
25 25 )
26 26 from .utils import (
27 27 stringutil,
28 28 )
29 29 stringio = util.stringio
30 30
31 31 # This is required for ncurses to display non-ASCII characters in default user
32 32 # locale encoding correctly. --immerrr
33 33 locale.setlocale(locale.LC_ALL, u'')
34 34
35 35 # patch comments based on the git one
36 36 diffhelptext = _("""# To remove '-' lines, make them ' ' lines (context).
37 37 # To remove '+' lines, delete them.
38 38 # Lines starting with # will be removed from the patch.
39 39 """)
40 40
41 41 hunkhelptext = _("""#
42 42 # If the patch applies cleanly, the edited hunk will immediately be
43 43 # added to the record list. If it does not apply cleanly, a rejects file
44 44 # will be generated. You can use that when you try again. If all lines
45 45 # of the hunk are removed, then the edit is aborted and the hunk is left
46 46 # unchanged.
47 47 """)
48 48
49 49 patchhelptext = _("""#
50 50 # If the patch applies cleanly, the edited patch will immediately
51 51 # be finalised. If it does not apply cleanly, rejects files will be
52 52 # generated. You can use those when you try again.
53 53 """)
54 54
55 55 try:
56 56 import curses
57 57 curses.error
58 58 except ImportError:
59 59 # I have no idea if wcurses works with crecord...
60 60 try:
61 61 import wcurses as curses
62 62 curses.error
63 63 except ImportError:
64 64 # wcurses is not shipped on Windows by default, or python is not
65 65 # compiled with curses
66 66 curses = False
67 67
68 68 class fallbackerror(error.Abort):
69 69 """Error that indicates the client should try to fallback to text mode."""
70 70 # Inherits from error.Abort so that existing behavior is preserved if the
71 71 # calling code does not know how to fallback.
72 72
73 73 def checkcurses(ui):
74 74 """Return True if the user wants to use curses
75 75
76 76 This method returns True if curses is found (and that python is built with
77 77 it) and that the user has the correct flag for the ui.
78 78 """
79 79 return curses and ui.interface("chunkselector") == "curses"
80 80
81 81 class patchnode(object):
82 82 """abstract class for patch graph nodes
83 83 (i.e. patchroot, header, hunk, hunkline)
84 84 """
85 85
86 86 def firstchild(self):
87 87 raise NotImplementedError("method must be implemented by subclass")
88 88
89 89 def lastchild(self):
90 90 raise NotImplementedError("method must be implemented by subclass")
91 91
92 92 def allchildren(self):
93 93 "Return a list of all of the direct children of this node"
94 94 raise NotImplementedError("method must be implemented by subclass")
95 95
96 96 def nextsibling(self):
97 97 """
98 98 Return the closest next item of the same type where there are no items
99 99 of different types between the current item and this closest item.
100 100 If no such item exists, return None.
101 101 """
102 102 raise NotImplementedError("method must be implemented by subclass")
103 103
104 104 def prevsibling(self):
105 105 """
106 106 Return the closest previous item of the same type where there are no
107 107 items of different types between the current item and this closest item.
108 108 If no such item exists, return None.
109 109 """
110 110 raise NotImplementedError("method must be implemented by subclass")
111 111
112 112 def parentitem(self):
113 113 raise NotImplementedError("method must be implemented by subclass")
114 114
115 115 def nextitem(self, skipfolded=True):
116 116 """
117 117 Try to return the next item closest to this item, regardless of item's
118 118 type (header, hunk, or hunkline).
119 119
120 120 If skipfolded == True, and the current item is folded, then the child
121 121 items that are hidden due to folding will be skipped when determining
122 122 the next item.
123 123
124 124 If it is not possible to get the next item, return None.
125 125 """
126 126 try:
127 127 itemfolded = self.folded
128 128 except AttributeError:
129 129 itemfolded = False
130 130 if skipfolded and itemfolded:
131 131 nextitem = self.nextsibling()
132 132 if nextitem is None:
133 133 try:
134 134 nextitem = self.parentitem().nextsibling()
135 135 except AttributeError:
136 136 nextitem = None
137 137 return nextitem
138 138 else:
139 139 # try child
140 140 item = self.firstchild()
141 141 if item is not None:
142 142 return item
143 143
144 144 # else try next sibling
145 145 item = self.nextsibling()
146 146 if item is not None:
147 147 return item
148 148
149 149 try:
150 150 # else try parent's next sibling
151 151 item = self.parentitem().nextsibling()
152 152 if item is not None:
153 153 return item
154 154
155 155 # else return grandparent's next sibling (or None)
156 156 return self.parentitem().parentitem().nextsibling()
157 157
158 158 except AttributeError: # parent and/or grandparent was None
159 159 return None
160 160
161 161 def previtem(self):
162 162 """
163 163 Try to return the previous item closest to this item, regardless of
164 164 item's type (header, hunk, or hunkline).
165 165
166 166 If it is not possible to get the previous item, return None.
167 167 """
168 168 # try previous sibling's last child's last child,
169 169 # else try previous sibling's last child, else try previous sibling
170 170 prevsibling = self.prevsibling()
171 171 if prevsibling is not None:
172 172 prevsiblinglastchild = prevsibling.lastchild()
173 173 if ((prevsiblinglastchild is not None) and
174 174 not prevsibling.folded):
175 175 prevsiblinglclc = prevsiblinglastchild.lastchild()
176 176 if ((prevsiblinglclc is not None) and
177 177 not prevsiblinglastchild.folded):
178 178 return prevsiblinglclc
179 179 else:
180 180 return prevsiblinglastchild
181 181 else:
182 182 return prevsibling
183 183
184 184 # try parent (or None)
185 185 return self.parentitem()
186 186
187 187 class patch(patchnode, list): # todo: rename patchroot
188 188 """
189 189 list of header objects representing the patch.
190 190 """
191 191 def __init__(self, headerlist):
192 192 self.extend(headerlist)
193 193 # add parent patch object reference to each header
194 194 for header in self:
195 195 header.patch = self
196 196
197 197 class uiheader(patchnode):
198 198 """patch header
199 199
200 200 xxx shouldn't we move this to mercurial/patch.py ?
201 201 """
202 202
203 203 def __init__(self, header):
204 204 self.nonuiheader = header
205 205 # flag to indicate whether to apply this chunk
206 206 self.applied = True
207 207 # flag which only affects the status display indicating if a node's
208 208 # children are partially applied (i.e. some applied, some not).
209 209 self.partial = False
210 210
211 211 # flag to indicate whether to display as folded/unfolded to user
212 212 self.folded = True
213 213
214 214 # list of all headers in patch
215 215 self.patch = None
216 216
217 217 # flag is False if this header was ever unfolded from initial state
218 218 self.neverunfolded = True
219 219 self.hunks = [uihunk(h, self) for h in self.hunks]
220 220
221 221 def prettystr(self):
222 222 x = stringio()
223 223 self.pretty(x)
224 224 return x.getvalue()
225 225
226 226 def nextsibling(self):
227 227 numheadersinpatch = len(self.patch)
228 228 indexofthisheader = self.patch.index(self)
229 229
230 230 if indexofthisheader < numheadersinpatch - 1:
231 231 nextheader = self.patch[indexofthisheader + 1]
232 232 return nextheader
233 233 else:
234 234 return None
235 235
236 236 def prevsibling(self):
237 237 indexofthisheader = self.patch.index(self)
238 238 if indexofthisheader > 0:
239 239 previousheader = self.patch[indexofthisheader - 1]
240 240 return previousheader
241 241 else:
242 242 return None
243 243
244 244 def parentitem(self):
245 245 """
246 246 there is no 'real' parent item of a header that can be selected,
247 247 so return None.
248 248 """
249 249 return None
250 250
251 251 def firstchild(self):
252 252 "return the first child of this item, if one exists. otherwise None."
253 253 if len(self.hunks) > 0:
254 254 return self.hunks[0]
255 255 else:
256 256 return None
257 257
258 258 def lastchild(self):
259 259 "return the last child of this item, if one exists. otherwise None."
260 260 if len(self.hunks) > 0:
261 261 return self.hunks[-1]
262 262 else:
263 263 return None
264 264
265 265 def allchildren(self):
266 266 "return a list of all of the direct children of this node"
267 267 return self.hunks
268 268
269 269 def __getattr__(self, name):
270 270 return getattr(self.nonuiheader, name)
271 271
272 272 class uihunkline(patchnode):
273 273 "represents a changed line in a hunk"
274 274 def __init__(self, linetext, hunk):
275 275 self.linetext = linetext
276 276 self.applied = True
277 277 # the parent hunk to which this line belongs
278 278 self.hunk = hunk
279 279 # folding lines currently is not used/needed, but this flag is needed
280 280 # in the previtem method.
281 281 self.folded = False
282 282
283 283 def prettystr(self):
284 284 return self.linetext
285 285
286 286 def nextsibling(self):
287 287 numlinesinhunk = len(self.hunk.changedlines)
288 288 indexofthisline = self.hunk.changedlines.index(self)
289 289
290 290 if (indexofthisline < numlinesinhunk - 1):
291 291 nextline = self.hunk.changedlines[indexofthisline + 1]
292 292 return nextline
293 293 else:
294 294 return None
295 295
296 296 def prevsibling(self):
297 297 indexofthisline = self.hunk.changedlines.index(self)
298 298 if indexofthisline > 0:
299 299 previousline = self.hunk.changedlines[indexofthisline - 1]
300 300 return previousline
301 301 else:
302 302 return None
303 303
304 304 def parentitem(self):
305 305 "return the parent to the current item"
306 306 return self.hunk
307 307
308 308 def firstchild(self):
309 309 "return the first child of this item, if one exists. otherwise None."
310 310 # hunk-lines don't have children
311 311 return None
312 312
313 313 def lastchild(self):
314 314 "return the last child of this item, if one exists. otherwise None."
315 315 # hunk-lines don't have children
316 316 return None
317 317
318 318 class uihunk(patchnode):
319 319 """ui patch hunk, wraps a hunk and keep track of ui behavior """
320 320 maxcontext = 3
321 321
322 322 def __init__(self, hunk, header):
323 323 self._hunk = hunk
324 324 self.changedlines = [uihunkline(line, self) for line in hunk.hunk]
325 325 self.header = header
326 326 # used at end for detecting how many removed lines were un-applied
327 327 self.originalremoved = self.removed
328 328
329 329 # flag to indicate whether to display as folded/unfolded to user
330 330 self.folded = True
331 331 # flag to indicate whether to apply this chunk
332 332 self.applied = True
333 333 # flag which only affects the status display indicating if a node's
334 334 # children are partially applied (i.e. some applied, some not).
335 335 self.partial = False
336 336
337 337 def nextsibling(self):
338 338 numhunksinheader = len(self.header.hunks)
339 339 indexofthishunk = self.header.hunks.index(self)
340 340
341 341 if (indexofthishunk < numhunksinheader - 1):
342 342 nexthunk = self.header.hunks[indexofthishunk + 1]
343 343 return nexthunk
344 344 else:
345 345 return None
346 346
347 347 def prevsibling(self):
348 348 indexofthishunk = self.header.hunks.index(self)
349 349 if indexofthishunk > 0:
350 350 previoushunk = self.header.hunks[indexofthishunk - 1]
351 351 return previoushunk
352 352 else:
353 353 return None
354 354
355 355 def parentitem(self):
356 356 "return the parent to the current item"
357 357 return self.header
358 358
359 359 def firstchild(self):
360 360 "return the first child of this item, if one exists. otherwise None."
361 361 if len(self.changedlines) > 0:
362 362 return self.changedlines[0]
363 363 else:
364 364 return None
365 365
366 366 def lastchild(self):
367 367 "return the last child of this item, if one exists. otherwise None."
368 368 if len(self.changedlines) > 0:
369 369 return self.changedlines[-1]
370 370 else:
371 371 return None
372 372
373 373 def allchildren(self):
374 374 "return a list of all of the direct children of this node"
375 375 return self.changedlines
376 376
377 377 def countchanges(self):
378 378 """changedlines -> (n+,n-)"""
379 379 add = len([l for l in self.changedlines if l.applied
380 380 and l.prettystr()[0] == '+'])
381 381 rem = len([l for l in self.changedlines if l.applied
382 382 and l.prettystr()[0] == '-'])
383 383 return add, rem
384 384
385 385 def getfromtoline(self):
386 386 # calculate the number of removed lines converted to context lines
387 387 removedconvertedtocontext = self.originalremoved - self.removed
388 388
389 389 contextlen = (len(self.before) + len(self.after) +
390 390 removedconvertedtocontext)
391 391 if self.after and self.after[-1] == '\\ No newline at end of file\n':
392 392 contextlen -= 1
393 393 fromlen = contextlen + self.removed
394 394 tolen = contextlen + self.added
395 395
396 396 # diffutils manual, section "2.2.2.2 detailed description of unified
397 397 # format": "an empty hunk is considered to end at the line that
398 398 # precedes the hunk."
399 399 #
400 400 # so, if either of hunks is empty, decrease its line start. --immerrr
401 401 # but only do this if fromline > 0, to avoid having, e.g fromline=-1.
402 402 fromline, toline = self.fromline, self.toline
403 403 if fromline != 0:
404 404 if fromlen == 0:
405 405 fromline -= 1
406 if tolen == 0:
406 if tolen == 0 and toline > 0:
407 407 toline -= 1
408 408
409 409 fromtoline = '@@ -%d,%d +%d,%d @@%s\n' % (
410 410 fromline, fromlen, toline, tolen,
411 411 self.proc and (' ' + self.proc))
412 412 return fromtoline
413 413
414 414 def write(self, fp):
415 415 # updated self.added/removed, which are used by getfromtoline()
416 416 self.added, self.removed = self.countchanges()
417 417 fp.write(self.getfromtoline())
418 418
419 419 hunklinelist = []
420 420 # add the following to the list: (1) all applied lines, and
421 421 # (2) all unapplied removal lines (convert these to context lines)
422 422 for changedline in self.changedlines:
423 423 changedlinestr = changedline.prettystr()
424 424 if changedline.applied:
425 425 hunklinelist.append(changedlinestr)
426 426 elif changedlinestr[0] == "-":
427 427 hunklinelist.append(" " + changedlinestr[1:])
428 428
429 429 fp.write(''.join(self.before + hunklinelist + self.after))
430 430
431 431 pretty = write
432 432
433 433 def prettystr(self):
434 434 x = stringio()
435 435 self.pretty(x)
436 436 return x.getvalue()
437 437
438 438 def reversehunk(self):
439 439 """return a recordhunk which is the reverse of the hunk
440 440
441 441 Assuming the displayed patch is diff(A, B) result. The returned hunk is
442 442 intended to be applied to B, instead of A.
443 443
444 444 For example, when A is "0\n1\n2\n6\n" and B is "0\n3\n4\n5\n6\n", and
445 445 the user made the following selection:
446 446
447 447 0
448 448 [x] -1 [x]: selected
449 449 [ ] -2 [ ]: not selected
450 450 [x] +3
451 451 [ ] +4
452 452 [x] +5
453 453 6
454 454
455 455 This function returns a hunk like:
456 456
457 457 0
458 458 -3
459 459 -4
460 460 -5
461 461 +1
462 462 +4
463 463 6
464 464
465 465 Note "4" was first deleted then added. That's because "4" exists in B
466 466 side and "-4" must exist between "-3" and "-5" to make the patch
467 467 applicable to B.
468 468 """
469 469 dels = []
470 470 adds = []
471 471 for line in self.changedlines:
472 472 text = line.linetext
473 473 if line.applied:
474 474 if text[0] == '+':
475 475 dels.append(text[1:])
476 476 elif text[0] == '-':
477 477 adds.append(text[1:])
478 478 elif text[0] == '+':
479 479 dels.append(text[1:])
480 480 adds.append(text[1:])
481 481 hunk = ['-%s' % l for l in dels] + ['+%s' % l for l in adds]
482 482 h = self._hunk
483 483 return patchmod.recordhunk(h.header, h.toline, h.fromline, h.proc,
484 484 h.before, hunk, h.after)
485 485
486 486 def __getattr__(self, name):
487 487 return getattr(self._hunk, name)
488 488
489 489 def __repr__(self):
490 490 return '<hunk %r@%d>' % (self.filename(), self.fromline)
491 491
492 492 def filterpatch(ui, chunks, chunkselector, operation=None):
493 493 """interactively filter patch chunks into applied-only chunks"""
494 494 chunks = list(chunks)
495 495 # convert chunks list into structure suitable for displaying/modifying
496 496 # with curses. create a list of headers only.
497 497 headers = [c for c in chunks if isinstance(c, patchmod.header)]
498 498
499 499 # if there are no changed files
500 500 if len(headers) == 0:
501 501 return [], {}
502 502 uiheaders = [uiheader(h) for h in headers]
503 503 # let user choose headers/hunks/lines, and mark their applied flags
504 504 # accordingly
505 505 ret = chunkselector(ui, uiheaders, operation=operation)
506 506 appliedhunklist = []
507 507 for hdr in uiheaders:
508 508 if (hdr.applied and
509 509 (hdr.special() or len([h for h in hdr.hunks if h.applied]) > 0)):
510 510 appliedhunklist.append(hdr)
511 511 fixoffset = 0
512 512 for hnk in hdr.hunks:
513 513 if hnk.applied:
514 514 appliedhunklist.append(hnk)
515 515 # adjust the 'to'-line offset of the hunk to be correct
516 516 # after de-activating some of the other hunks for this file
517 517 if fixoffset:
518 518 #hnk = copy.copy(hnk) # necessary??
519 519 hnk.toline += fixoffset
520 520 else:
521 521 fixoffset += hnk.removed - hnk.added
522 522
523 523 return (appliedhunklist, ret)
524 524
525 525 def chunkselector(ui, headerlist, operation=None):
526 526 """
527 527 curses interface to get selection of chunks, and mark the applied flags
528 528 of the chosen chunks.
529 529 """
530 530 ui.write(_('starting interactive selection\n'))
531 531 chunkselector = curseschunkselector(headerlist, ui, operation)
532 532 origsigtstp = sentinel = object()
533 533 if util.safehasattr(signal, 'SIGTSTP'):
534 534 origsigtstp = signal.getsignal(signal.SIGTSTP)
535 535 try:
536 536 curses.wrapper(chunkselector.main)
537 537 if chunkselector.initexc is not None:
538 538 raise chunkselector.initexc
539 539 # ncurses does not restore signal handler for SIGTSTP
540 540 finally:
541 541 if origsigtstp is not sentinel:
542 542 signal.signal(signal.SIGTSTP, origsigtstp)
543 543 return chunkselector.opts
544 544
545 545 def testdecorator(testfn, f):
546 546 def u(*args, **kwargs):
547 547 return f(testfn, *args, **kwargs)
548 548 return u
549 549
550 550 def testchunkselector(testfn, ui, headerlist, operation=None):
551 551 """
552 552 test interface to get selection of chunks, and mark the applied flags
553 553 of the chosen chunks.
554 554 """
555 555 chunkselector = curseschunkselector(headerlist, ui, operation)
556 556 if testfn and os.path.exists(testfn):
557 557 testf = open(testfn, 'rb')
558 558 testcommands = [x.rstrip('\n') for x in testf.readlines()]
559 559 testf.close()
560 560 while True:
561 561 if chunkselector.handlekeypressed(testcommands.pop(0), test=True):
562 562 break
563 563 return chunkselector.opts
564 564
565 565 _headermessages = { # {operation: text}
566 566 'apply': _('Select hunks to apply'),
567 567 'discard': _('Select hunks to discard'),
568 568 None: _('Select hunks to record'),
569 569 }
570 570
571 571 class curseschunkselector(object):
572 572 def __init__(self, headerlist, ui, operation=None):
573 573 # put the headers into a patch object
574 574 self.headerlist = patch(headerlist)
575 575
576 576 self.ui = ui
577 577 self.opts = {}
578 578
579 579 self.errorstr = None
580 580 # list of all chunks
581 581 self.chunklist = []
582 582 for h in headerlist:
583 583 self.chunklist.append(h)
584 584 self.chunklist.extend(h.hunks)
585 585
586 586 # dictionary mapping (fgcolor, bgcolor) pairs to the
587 587 # corresponding curses color-pair value.
588 588 self.colorpairs = {}
589 589 # maps custom nicknames of color-pairs to curses color-pair values
590 590 self.colorpairnames = {}
591 591
592 592 # Honor color setting of ui section. Keep colored setup as
593 593 # long as not explicitly set to a falsy value - especially,
594 594 # when not set at all. This is to stay most compatible with
595 595 # previous (color only) behaviour.
596 596 uicolor = stringutil.parsebool(self.ui.config('ui', 'color'))
597 597 self.usecolor = uicolor is not False
598 598
599 599 # the currently selected header, hunk, or hunk-line
600 600 self.currentselecteditem = self.headerlist[0]
601 601
602 602 # updated when printing out patch-display -- the 'lines' here are the
603 603 # line positions *in the pad*, not on the screen.
604 604 self.selecteditemstartline = 0
605 605 self.selecteditemendline = None
606 606
607 607 # define indentation levels
608 608 self.headerindentnumchars = 0
609 609 self.hunkindentnumchars = 3
610 610 self.hunklineindentnumchars = 6
611 611
612 612 # the first line of the pad to print to the screen
613 613 self.firstlineofpadtoprint = 0
614 614
615 615 # keeps track of the number of lines in the pad
616 616 self.numpadlines = None
617 617
618 618 self.numstatuslines = 1
619 619
620 620 # keep a running count of the number of lines printed to the pad
621 621 # (used for determining when the selected item begins/ends)
622 622 self.linesprintedtopadsofar = 0
623 623
624 624 # the first line of the pad which is visible on the screen
625 625 self.firstlineofpadtoprint = 0
626 626
627 627 # stores optional text for a commit comment provided by the user
628 628 self.commenttext = ""
629 629
630 630 # if the last 'toggle all' command caused all changes to be applied
631 631 self.waslasttoggleallapplied = True
632 632
633 633 # affects some ui text
634 634 if operation not in _headermessages:
635 635 raise error.ProgrammingError('unexpected operation: %s' % operation)
636 636 self.operation = operation
637 637
638 638 def uparrowevent(self):
639 639 """
640 640 try to select the previous item to the current item that has the
641 641 most-indented level. for example, if a hunk is selected, try to select
642 642 the last hunkline of the hunk prior to the selected hunk. or, if
643 643 the first hunkline of a hunk is currently selected, then select the
644 644 hunk itself.
645 645 """
646 646 currentitem = self.currentselecteditem
647 647
648 648 nextitem = currentitem.previtem()
649 649
650 650 if nextitem is None:
651 651 # if no parent item (i.e. currentitem is the first header), then
652 652 # no change...
653 653 nextitem = currentitem
654 654
655 655 self.currentselecteditem = nextitem
656 656
657 657 def uparrowshiftevent(self):
658 658 """
659 659 select (if possible) the previous item on the same level as the
660 660 currently selected item. otherwise, select (if possible) the
661 661 parent-item of the currently selected item.
662 662 """
663 663 currentitem = self.currentselecteditem
664 664 nextitem = currentitem.prevsibling()
665 665 # if there's no previous sibling, try choosing the parent
666 666 if nextitem is None:
667 667 nextitem = currentitem.parentitem()
668 668 if nextitem is None:
669 669 # if no parent item (i.e. currentitem is the first header), then
670 670 # no change...
671 671 nextitem = currentitem
672 672
673 673 self.currentselecteditem = nextitem
674 674 self.recenterdisplayedarea()
675 675
676 676 def downarrowevent(self):
677 677 """
678 678 try to select the next item to the current item that has the
679 679 most-indented level. for example, if a hunk is selected, select
680 680 the first hunkline of the selected hunk. or, if the last hunkline of
681 681 a hunk is currently selected, then select the next hunk, if one exists,
682 682 or if not, the next header if one exists.
683 683 """
684 684 #self.startprintline += 1 #debug
685 685 currentitem = self.currentselecteditem
686 686
687 687 nextitem = currentitem.nextitem()
688 688 # if there's no next item, keep the selection as-is
689 689 if nextitem is None:
690 690 nextitem = currentitem
691 691
692 692 self.currentselecteditem = nextitem
693 693
694 694 def downarrowshiftevent(self):
695 695 """
696 696 select (if possible) the next item on the same level as the currently
697 697 selected item. otherwise, select (if possible) the next item on the
698 698 same level as the parent item of the currently selected item.
699 699 """
700 700 currentitem = self.currentselecteditem
701 701 nextitem = currentitem.nextsibling()
702 702 # if there's no next sibling, try choosing the parent's nextsibling
703 703 if nextitem is None:
704 704 try:
705 705 nextitem = currentitem.parentitem().nextsibling()
706 706 except AttributeError:
707 707 # parentitem returned None, so nextsibling() can't be called
708 708 nextitem = None
709 709 if nextitem is None:
710 710 # if parent has no next sibling, then no change...
711 711 nextitem = currentitem
712 712
713 713 self.currentselecteditem = nextitem
714 714 self.recenterdisplayedarea()
715 715
716 716 def rightarrowevent(self):
717 717 """
718 718 select (if possible) the first of this item's child-items.
719 719 """
720 720 currentitem = self.currentselecteditem
721 721 nextitem = currentitem.firstchild()
722 722
723 723 # turn off folding if we want to show a child-item
724 724 if currentitem.folded:
725 725 self.togglefolded(currentitem)
726 726
727 727 if nextitem is None:
728 728 # if no next item on parent-level, then no change...
729 729 nextitem = currentitem
730 730
731 731 self.currentselecteditem = nextitem
732 732
733 733 def leftarrowevent(self):
734 734 """
735 735 if the current item can be folded (i.e. it is an unfolded header or
736 736 hunk), then fold it. otherwise try select (if possible) the parent
737 737 of this item.
738 738 """
739 739 currentitem = self.currentselecteditem
740 740
741 741 # try to fold the item
742 742 if not isinstance(currentitem, uihunkline):
743 743 if not currentitem.folded:
744 744 self.togglefolded(item=currentitem)
745 745 return
746 746
747 747 # if it can't be folded, try to select the parent item
748 748 nextitem = currentitem.parentitem()
749 749
750 750 if nextitem is None:
751 751 # if no item on parent-level, then no change...
752 752 nextitem = currentitem
753 753 if not nextitem.folded:
754 754 self.togglefolded(item=nextitem)
755 755
756 756 self.currentselecteditem = nextitem
757 757
758 758 def leftarrowshiftevent(self):
759 759 """
760 760 select the header of the current item (or fold current item if the
761 761 current item is already a header).
762 762 """
763 763 currentitem = self.currentselecteditem
764 764
765 765 if isinstance(currentitem, uiheader):
766 766 if not currentitem.folded:
767 767 self.togglefolded(item=currentitem)
768 768 return
769 769
770 770 # select the parent item recursively until we're at a header
771 771 while True:
772 772 nextitem = currentitem.parentitem()
773 773 if nextitem is None:
774 774 break
775 775 else:
776 776 currentitem = nextitem
777 777
778 778 self.currentselecteditem = currentitem
779 779
780 780 def updatescroll(self):
781 781 "scroll the screen to fully show the currently-selected"
782 782 selstart = self.selecteditemstartline
783 783 selend = self.selecteditemendline
784 784
785 785 padstart = self.firstlineofpadtoprint
786 786 padend = padstart + self.yscreensize - self.numstatuslines - 1
787 787 # 'buffered' pad start/end values which scroll with a certain
788 788 # top/bottom context margin
789 789 padstartbuffered = padstart + 3
790 790 padendbuffered = padend - 3
791 791
792 792 if selend > padendbuffered:
793 793 self.scrolllines(selend - padendbuffered)
794 794 elif selstart < padstartbuffered:
795 795 # negative values scroll in pgup direction
796 796 self.scrolllines(selstart - padstartbuffered)
797 797
798 798 def scrolllines(self, numlines):
799 799 "scroll the screen up (down) by numlines when numlines >0 (<0)."
800 800 self.firstlineofpadtoprint += numlines
801 801 if self.firstlineofpadtoprint < 0:
802 802 self.firstlineofpadtoprint = 0
803 803 if self.firstlineofpadtoprint > self.numpadlines - 1:
804 804 self.firstlineofpadtoprint = self.numpadlines - 1
805 805
806 806 def toggleapply(self, item=None):
807 807 """
808 808 toggle the applied flag of the specified item. if no item is specified,
809 809 toggle the flag of the currently selected item.
810 810 """
811 811 if item is None:
812 812 item = self.currentselecteditem
813 813
814 814 item.applied = not item.applied
815 815
816 816 if isinstance(item, uiheader):
817 817 item.partial = False
818 818 if item.applied:
819 819 # apply all its hunks
820 820 for hnk in item.hunks:
821 821 hnk.applied = True
822 822 # apply all their hunklines
823 823 for hunkline in hnk.changedlines:
824 824 hunkline.applied = True
825 825 else:
826 826 # un-apply all its hunks
827 827 for hnk in item.hunks:
828 828 hnk.applied = False
829 829 hnk.partial = False
830 830 # un-apply all their hunklines
831 831 for hunkline in hnk.changedlines:
832 832 hunkline.applied = False
833 833 elif isinstance(item, uihunk):
834 834 item.partial = False
835 835 # apply all it's hunklines
836 836 for hunkline in item.changedlines:
837 837 hunkline.applied = item.applied
838 838
839 839 siblingappliedstatus = [hnk.applied for hnk in item.header.hunks]
840 840 allsiblingsapplied = not (False in siblingappliedstatus)
841 841 nosiblingsapplied = not (True in siblingappliedstatus)
842 842
843 843 siblingspartialstatus = [hnk.partial for hnk in item.header.hunks]
844 844 somesiblingspartial = (True in siblingspartialstatus)
845 845
846 846 #cases where applied or partial should be removed from header
847 847
848 848 # if no 'sibling' hunks are applied (including this hunk)
849 849 if nosiblingsapplied:
850 850 if not item.header.special():
851 851 item.header.applied = False
852 852 item.header.partial = False
853 853 else: # some/all parent siblings are applied
854 854 item.header.applied = True
855 855 item.header.partial = (somesiblingspartial or
856 856 not allsiblingsapplied)
857 857
858 858 elif isinstance(item, uihunkline):
859 859 siblingappliedstatus = [ln.applied for ln in item.hunk.changedlines]
860 860 allsiblingsapplied = not (False in siblingappliedstatus)
861 861 nosiblingsapplied = not (True in siblingappliedstatus)
862 862
863 863 # if no 'sibling' lines are applied
864 864 if nosiblingsapplied:
865 865 item.hunk.applied = False
866 866 item.hunk.partial = False
867 867 elif allsiblingsapplied:
868 868 item.hunk.applied = True
869 869 item.hunk.partial = False
870 870 else: # some siblings applied
871 871 item.hunk.applied = True
872 872 item.hunk.partial = True
873 873
874 874 parentsiblingsapplied = [hnk.applied for hnk
875 875 in item.hunk.header.hunks]
876 876 noparentsiblingsapplied = not (True in parentsiblingsapplied)
877 877 allparentsiblingsapplied = not (False in parentsiblingsapplied)
878 878
879 879 parentsiblingspartial = [hnk.partial for hnk
880 880 in item.hunk.header.hunks]
881 881 someparentsiblingspartial = (True in parentsiblingspartial)
882 882
883 883 # if all parent hunks are not applied, un-apply header
884 884 if noparentsiblingsapplied:
885 885 if not item.hunk.header.special():
886 886 item.hunk.header.applied = False
887 887 item.hunk.header.partial = False
888 888 # set the applied and partial status of the header if needed
889 889 else: # some/all parent siblings are applied
890 890 item.hunk.header.applied = True
891 891 item.hunk.header.partial = (someparentsiblingspartial or
892 892 not allparentsiblingsapplied)
893 893
894 894 def toggleall(self):
895 895 "toggle the applied flag of all items."
896 896 if self.waslasttoggleallapplied: # then unapply them this time
897 897 for item in self.headerlist:
898 898 if item.applied:
899 899 self.toggleapply(item)
900 900 else:
901 901 for item in self.headerlist:
902 902 if not item.applied:
903 903 self.toggleapply(item)
904 904 self.waslasttoggleallapplied = not self.waslasttoggleallapplied
905 905
906 906 def togglefolded(self, item=None, foldparent=False):
907 907 "toggle folded flag of specified item (defaults to currently selected)"
908 908 if item is None:
909 909 item = self.currentselecteditem
910 910 if foldparent or (isinstance(item, uiheader) and item.neverunfolded):
911 911 if not isinstance(item, uiheader):
912 912 # we need to select the parent item in this case
913 913 self.currentselecteditem = item = item.parentitem()
914 914 elif item.neverunfolded:
915 915 item.neverunfolded = False
916 916
917 917 # also fold any foldable children of the parent/current item
918 918 if isinstance(item, uiheader): # the original or 'new' item
919 919 for child in item.allchildren():
920 920 child.folded = not item.folded
921 921
922 922 if isinstance(item, (uiheader, uihunk)):
923 923 item.folded = not item.folded
924 924
925 925 def alignstring(self, instr, window):
926 926 """
927 927 add whitespace to the end of a string in order to make it fill
928 928 the screen in the x direction. the current cursor position is
929 929 taken into account when making this calculation. the string can span
930 930 multiple lines.
931 931 """
932 932 y, xstart = window.getyx()
933 933 width = self.xscreensize
934 934 # turn tabs into spaces
935 935 instr = instr.expandtabs(4)
936 936 strwidth = encoding.colwidth(instr)
937 937 numspaces = (width - ((strwidth + xstart) % width) - 1)
938 938 return instr + " " * numspaces + "\n"
939 939
940 940 def printstring(self, window, text, fgcolor=None, bgcolor=None, pair=None,
941 941 pairname=None, attrlist=None, towin=True, align=True, showwhtspc=False):
942 942 """
943 943 print the string, text, with the specified colors and attributes, to
944 944 the specified curses window object.
945 945
946 946 the foreground and background colors are of the form
947 947 curses.color_xxxx, where xxxx is one of: [black, blue, cyan, green,
948 948 magenta, red, white, yellow]. if pairname is provided, a color
949 949 pair will be looked up in the self.colorpairnames dictionary.
950 950
951 951 attrlist is a list containing text attributes in the form of
952 952 curses.a_xxxx, where xxxx can be: [bold, dim, normal, standout,
953 953 underline].
954 954
955 955 if align == True, whitespace is added to the printed string such that
956 956 the string stretches to the right border of the window.
957 957
958 958 if showwhtspc == True, trailing whitespace of a string is highlighted.
959 959 """
960 960 # preprocess the text, converting tabs to spaces
961 961 text = text.expandtabs(4)
962 962 # strip \n, and convert control characters to ^[char] representation
963 963 text = re.sub(br'[\x00-\x08\x0a-\x1f]',
964 964 lambda m:'^' + chr(ord(m.group()) + 64), text.strip('\n'))
965 965
966 966 if pair is not None:
967 967 colorpair = pair
968 968 elif pairname is not None:
969 969 colorpair = self.colorpairnames[pairname]
970 970 else:
971 971 if fgcolor is None:
972 972 fgcolor = -1
973 973 if bgcolor is None:
974 974 bgcolor = -1
975 975 if (fgcolor, bgcolor) in self.colorpairs:
976 976 colorpair = self.colorpairs[(fgcolor, bgcolor)]
977 977 else:
978 978 colorpair = self.getcolorpair(fgcolor, bgcolor)
979 979 # add attributes if possible
980 980 if attrlist is None:
981 981 attrlist = []
982 982 if colorpair < 256:
983 983 # then it is safe to apply all attributes
984 984 for textattr in attrlist:
985 985 colorpair |= textattr
986 986 else:
987 987 # just apply a select few (safe?) attributes
988 988 for textattr in (curses.A_UNDERLINE, curses.A_BOLD):
989 989 if textattr in attrlist:
990 990 colorpair |= textattr
991 991
992 992 y, xstart = self.chunkpad.getyx()
993 993 t = "" # variable for counting lines printed
994 994 # if requested, show trailing whitespace
995 995 if showwhtspc:
996 996 origlen = len(text)
997 997 text = text.rstrip(' \n') # tabs have already been expanded
998 998 strippedlen = len(text)
999 999 numtrailingspaces = origlen - strippedlen
1000 1000
1001 1001 if towin:
1002 1002 window.addstr(text, colorpair)
1003 1003 t += text
1004 1004
1005 1005 if showwhtspc:
1006 1006 wscolorpair = colorpair | curses.A_REVERSE
1007 1007 if towin:
1008 1008 for i in range(numtrailingspaces):
1009 1009 window.addch(curses.ACS_CKBOARD, wscolorpair)
1010 1010 t += " " * numtrailingspaces
1011 1011
1012 1012 if align:
1013 1013 if towin:
1014 1014 extrawhitespace = self.alignstring("", window)
1015 1015 window.addstr(extrawhitespace, colorpair)
1016 1016 else:
1017 1017 # need to use t, since the x position hasn't incremented
1018 1018 extrawhitespace = self.alignstring(t, window)
1019 1019 t += extrawhitespace
1020 1020
1021 1021 # is reset to 0 at the beginning of printitem()
1022 1022
1023 1023 linesprinted = (xstart + len(t)) / self.xscreensize
1024 1024 self.linesprintedtopadsofar += linesprinted
1025 1025 return t
1026 1026
1027 1027 def _getstatuslinesegments(self):
1028 1028 """-> [str]. return segments"""
1029 1029 selected = self.currentselecteditem.applied
1030 1030 spaceselect = _('space: select')
1031 1031 spacedeselect = _('space: deselect')
1032 1032 # Format the selected label into a place as long as the longer of the
1033 1033 # two possible labels. This may vary by language.
1034 1034 spacelen = max(len(spaceselect), len(spacedeselect))
1035 1035 selectedlabel = '%-*s' % (spacelen,
1036 1036 spacedeselect if selected else spaceselect)
1037 1037 segments = [
1038 1038 _headermessages[self.operation],
1039 1039 '-',
1040 1040 _('[x]=selected **=collapsed'),
1041 1041 _('c: confirm'),
1042 1042 _('q: abort'),
1043 1043 _('arrow keys: move/expand/collapse'),
1044 1044 selectedlabel,
1045 1045 _('?: help'),
1046 1046 ]
1047 1047 return segments
1048 1048
1049 1049 def _getstatuslines(self):
1050 1050 """() -> [str]. return short help used in the top status window"""
1051 1051 if self.errorstr is not None:
1052 1052 lines = [self.errorstr, _('Press any key to continue')]
1053 1053 else:
1054 1054 # wrap segments to lines
1055 1055 segments = self._getstatuslinesegments()
1056 1056 width = self.xscreensize
1057 1057 lines = []
1058 1058 lastwidth = width
1059 1059 for s in segments:
1060 1060 w = encoding.colwidth(s)
1061 1061 sep = ' ' * (1 + (s and s[0] not in '-['))
1062 1062 if lastwidth + w + len(sep) >= width:
1063 1063 lines.append(s)
1064 1064 lastwidth = w
1065 1065 else:
1066 1066 lines[-1] += sep + s
1067 1067 lastwidth += w + len(sep)
1068 1068 if len(lines) != self.numstatuslines:
1069 1069 self.numstatuslines = len(lines)
1070 1070 self.statuswin.resize(self.numstatuslines, self.xscreensize)
1071 1071 return [stringutil.ellipsis(l, self.xscreensize - 1) for l in lines]
1072 1072
1073 1073 def updatescreen(self):
1074 1074 self.statuswin.erase()
1075 1075 self.chunkpad.erase()
1076 1076
1077 1077 printstring = self.printstring
1078 1078
1079 1079 # print out the status lines at the top
1080 1080 try:
1081 1081 for line in self._getstatuslines():
1082 1082 printstring(self.statuswin, line, pairname="legend")
1083 1083 self.statuswin.refresh()
1084 1084 except curses.error:
1085 1085 pass
1086 1086 if self.errorstr is not None:
1087 1087 return
1088 1088
1089 1089 # print out the patch in the remaining part of the window
1090 1090 try:
1091 1091 self.printitem()
1092 1092 self.updatescroll()
1093 1093 self.chunkpad.refresh(self.firstlineofpadtoprint, 0,
1094 1094 self.numstatuslines, 0,
1095 1095 self.yscreensize - self.numstatuslines,
1096 1096 self.xscreensize)
1097 1097 except curses.error:
1098 1098 pass
1099 1099
1100 1100 def getstatusprefixstring(self, item):
1101 1101 """
1102 1102 create a string to prefix a line with which indicates whether 'item'
1103 1103 is applied and/or folded.
1104 1104 """
1105 1105
1106 1106 # create checkbox string
1107 1107 if item.applied:
1108 1108 if not isinstance(item, uihunkline) and item.partial:
1109 1109 checkbox = "[~]"
1110 1110 else:
1111 1111 checkbox = "[x]"
1112 1112 else:
1113 1113 checkbox = "[ ]"
1114 1114
1115 1115 try:
1116 1116 if item.folded:
1117 1117 checkbox += "**"
1118 1118 if isinstance(item, uiheader):
1119 1119 # one of "m", "a", or "d" (modified, added, deleted)
1120 1120 filestatus = item.changetype
1121 1121
1122 1122 checkbox += filestatus + " "
1123 1123 else:
1124 1124 checkbox += " "
1125 1125 if isinstance(item, uiheader):
1126 1126 # add two more spaces for headers
1127 1127 checkbox += " "
1128 1128 except AttributeError: # not foldable
1129 1129 checkbox += " "
1130 1130
1131 1131 return checkbox
1132 1132
1133 1133 def printheader(self, header, selected=False, towin=True,
1134 1134 ignorefolding=False):
1135 1135 """
1136 1136 print the header to the pad. if countlines is True, don't print
1137 1137 anything, but just count the number of lines which would be printed.
1138 1138 """
1139 1139
1140 1140 outstr = ""
1141 1141 text = header.prettystr()
1142 1142 chunkindex = self.chunklist.index(header)
1143 1143
1144 1144 if chunkindex != 0 and not header.folded:
1145 1145 # add separating line before headers
1146 1146 outstr += self.printstring(self.chunkpad, '_' * self.xscreensize,
1147 1147 towin=towin, align=False)
1148 1148 # select color-pair based on if the header is selected
1149 1149 colorpair = self.getcolorpair(name=selected and "selected" or "normal",
1150 1150 attrlist=[curses.A_BOLD])
1151 1151
1152 1152 # print out each line of the chunk, expanding it to screen width
1153 1153
1154 1154 # number of characters to indent lines on this level by
1155 1155 indentnumchars = 0
1156 1156 checkbox = self.getstatusprefixstring(header)
1157 1157 if not header.folded or ignorefolding:
1158 1158 textlist = text.split("\n")
1159 1159 linestr = checkbox + textlist[0]
1160 1160 else:
1161 1161 linestr = checkbox + header.filename()
1162 1162 outstr += self.printstring(self.chunkpad, linestr, pair=colorpair,
1163 1163 towin=towin)
1164 1164 if not header.folded or ignorefolding:
1165 1165 if len(textlist) > 1:
1166 1166 for line in textlist[1:]:
1167 1167 linestr = " "*(indentnumchars + len(checkbox)) + line
1168 1168 outstr += self.printstring(self.chunkpad, linestr,
1169 1169 pair=colorpair, towin=towin)
1170 1170
1171 1171 return outstr
1172 1172
1173 1173 def printhunklinesbefore(self, hunk, selected=False, towin=True,
1174 1174 ignorefolding=False):
1175 1175 "includes start/end line indicator"
1176 1176 outstr = ""
1177 1177 # where hunk is in list of siblings
1178 1178 hunkindex = hunk.header.hunks.index(hunk)
1179 1179
1180 1180 if hunkindex != 0:
1181 1181 # add separating line before headers
1182 1182 outstr += self.printstring(self.chunkpad, ' '*self.xscreensize,
1183 1183 towin=towin, align=False)
1184 1184
1185 1185 colorpair = self.getcolorpair(name=selected and "selected" or "normal",
1186 1186 attrlist=[curses.A_BOLD])
1187 1187
1188 1188 # print out from-to line with checkbox
1189 1189 checkbox = self.getstatusprefixstring(hunk)
1190 1190
1191 1191 lineprefix = " "*self.hunkindentnumchars + checkbox
1192 1192 frtoline = " " + hunk.getfromtoline().strip("\n")
1193 1193
1194 1194 outstr += self.printstring(self.chunkpad, lineprefix, towin=towin,
1195 1195 align=False) # add uncolored checkbox/indent
1196 1196 outstr += self.printstring(self.chunkpad, frtoline, pair=colorpair,
1197 1197 towin=towin)
1198 1198
1199 1199 if hunk.folded and not ignorefolding:
1200 1200 # skip remainder of output
1201 1201 return outstr
1202 1202
1203 1203 # print out lines of the chunk preceeding changed-lines
1204 1204 for line in hunk.before:
1205 1205 linestr = " "*(self.hunklineindentnumchars + len(checkbox)) + line
1206 1206 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1207 1207
1208 1208 return outstr
1209 1209
1210 1210 def printhunklinesafter(self, hunk, towin=True, ignorefolding=False):
1211 1211 outstr = ""
1212 1212 if hunk.folded and not ignorefolding:
1213 1213 return outstr
1214 1214
1215 1215 # a bit superfluous, but to avoid hard-coding indent amount
1216 1216 checkbox = self.getstatusprefixstring(hunk)
1217 1217 for line in hunk.after:
1218 1218 linestr = " "*(self.hunklineindentnumchars + len(checkbox)) + line
1219 1219 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1220 1220
1221 1221 return outstr
1222 1222
1223 1223 def printhunkchangedline(self, hunkline, selected=False, towin=True):
1224 1224 outstr = ""
1225 1225 checkbox = self.getstatusprefixstring(hunkline)
1226 1226
1227 1227 linestr = hunkline.prettystr().strip("\n")
1228 1228
1229 1229 # select color-pair based on whether line is an addition/removal
1230 1230 if selected:
1231 1231 colorpair = self.getcolorpair(name="selected")
1232 1232 elif linestr.startswith("+"):
1233 1233 colorpair = self.getcolorpair(name="addition")
1234 1234 elif linestr.startswith("-"):
1235 1235 colorpair = self.getcolorpair(name="deletion")
1236 1236 elif linestr.startswith("\\"):
1237 1237 colorpair = self.getcolorpair(name="normal")
1238 1238
1239 1239 lineprefix = " "*self.hunklineindentnumchars + checkbox
1240 1240 outstr += self.printstring(self.chunkpad, lineprefix, towin=towin,
1241 1241 align=False) # add uncolored checkbox/indent
1242 1242 outstr += self.printstring(self.chunkpad, linestr, pair=colorpair,
1243 1243 towin=towin, showwhtspc=True)
1244 1244 return outstr
1245 1245
1246 1246 def printitem(self, item=None, ignorefolding=False, recursechildren=True,
1247 1247 towin=True):
1248 1248 """
1249 1249 use __printitem() to print the the specified item.applied.
1250 1250 if item is not specified, then print the entire patch.
1251 1251 (hiding folded elements, etc. -- see __printitem() docstring)
1252 1252 """
1253 1253
1254 1254 if item is None:
1255 1255 item = self.headerlist
1256 1256 if recursechildren:
1257 1257 self.linesprintedtopadsofar = 0
1258 1258
1259 1259 outstr = []
1260 1260 self.__printitem(item, ignorefolding, recursechildren, outstr,
1261 1261 towin=towin)
1262 1262 return ''.join(outstr)
1263 1263
1264 1264 def outofdisplayedarea(self):
1265 1265 y, _ = self.chunkpad.getyx() # cursor location
1266 1266 # * 2 here works but an optimization would be the max number of
1267 1267 # consecutive non selectable lines
1268 1268 # i.e the max number of context line for any hunk in the patch
1269 1269 miny = min(0, self.firstlineofpadtoprint - self.yscreensize)
1270 1270 maxy = self.firstlineofpadtoprint + self.yscreensize * 2
1271 1271 return y < miny or y > maxy
1272 1272
1273 1273 def handleselection(self, item, recursechildren):
1274 1274 selected = (item is self.currentselecteditem)
1275 1275 if selected and recursechildren:
1276 1276 # assumes line numbering starting from line 0
1277 1277 self.selecteditemstartline = self.linesprintedtopadsofar
1278 1278 selecteditemlines = self.getnumlinesdisplayed(item,
1279 1279 recursechildren=False)
1280 1280 self.selecteditemendline = (self.selecteditemstartline +
1281 1281 selecteditemlines - 1)
1282 1282 return selected
1283 1283
1284 1284 def __printitem(self, item, ignorefolding, recursechildren, outstr,
1285 1285 towin=True):
1286 1286 """
1287 1287 recursive method for printing out patch/header/hunk/hunk-line data to
1288 1288 screen. also returns a string with all of the content of the displayed
1289 1289 patch (not including coloring, etc.).
1290 1290
1291 1291 if ignorefolding is True, then folded items are printed out.
1292 1292
1293 1293 if recursechildren is False, then only print the item without its
1294 1294 child items.
1295 1295 """
1296 1296
1297 1297 if towin and self.outofdisplayedarea():
1298 1298 return
1299 1299
1300 1300 selected = self.handleselection(item, recursechildren)
1301 1301
1302 1302 # patch object is a list of headers
1303 1303 if isinstance(item, patch):
1304 1304 if recursechildren:
1305 1305 for hdr in item:
1306 1306 self.__printitem(hdr, ignorefolding,
1307 1307 recursechildren, outstr, towin)
1308 1308 # todo: eliminate all isinstance() calls
1309 1309 if isinstance(item, uiheader):
1310 1310 outstr.append(self.printheader(item, selected, towin=towin,
1311 1311 ignorefolding=ignorefolding))
1312 1312 if recursechildren:
1313 1313 for hnk in item.hunks:
1314 1314 self.__printitem(hnk, ignorefolding,
1315 1315 recursechildren, outstr, towin)
1316 1316 elif (isinstance(item, uihunk) and
1317 1317 ((not item.header.folded) or ignorefolding)):
1318 1318 # print the hunk data which comes before the changed-lines
1319 1319 outstr.append(self.printhunklinesbefore(item, selected, towin=towin,
1320 1320 ignorefolding=ignorefolding))
1321 1321 if recursechildren:
1322 1322 for l in item.changedlines:
1323 1323 self.__printitem(l, ignorefolding,
1324 1324 recursechildren, outstr, towin)
1325 1325 outstr.append(self.printhunklinesafter(item, towin=towin,
1326 1326 ignorefolding=ignorefolding))
1327 1327 elif (isinstance(item, uihunkline) and
1328 1328 ((not item.hunk.folded) or ignorefolding)):
1329 1329 outstr.append(self.printhunkchangedline(item, selected,
1330 1330 towin=towin))
1331 1331
1332 1332 return outstr
1333 1333
1334 1334 def getnumlinesdisplayed(self, item=None, ignorefolding=False,
1335 1335 recursechildren=True):
1336 1336 """
1337 1337 return the number of lines which would be displayed if the item were
1338 1338 to be printed to the display. the item will not be printed to the
1339 1339 display (pad).
1340 1340 if no item is given, assume the entire patch.
1341 1341 if ignorefolding is True, folded items will be unfolded when counting
1342 1342 the number of lines.
1343 1343 """
1344 1344
1345 1345 # temporarily disable printing to windows by printstring
1346 1346 patchdisplaystring = self.printitem(item, ignorefolding,
1347 1347 recursechildren, towin=False)
1348 1348 numlines = len(patchdisplaystring) // self.xscreensize
1349 1349 return numlines
1350 1350
1351 1351 def sigwinchhandler(self, n, frame):
1352 1352 "handle window resizing"
1353 1353 try:
1354 1354 curses.endwin()
1355 1355 self.xscreensize, self.yscreensize = scmutil.termsize(self.ui)
1356 1356 self.statuswin.resize(self.numstatuslines, self.xscreensize)
1357 1357 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1358 1358 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1359 1359 except curses.error:
1360 1360 pass
1361 1361
1362 1362 def getcolorpair(self, fgcolor=None, bgcolor=None, name=None,
1363 1363 attrlist=None):
1364 1364 """
1365 1365 get a curses color pair, adding it to self.colorpairs if it is not
1366 1366 already defined. an optional string, name, can be passed as a shortcut
1367 1367 for referring to the color-pair. by default, if no arguments are
1368 1368 specified, the white foreground / black background color-pair is
1369 1369 returned.
1370 1370
1371 1371 it is expected that this function will be used exclusively for
1372 1372 initializing color pairs, and not curses.init_pair().
1373 1373
1374 1374 attrlist is used to 'flavor' the returned color-pair. this information
1375 1375 is not stored in self.colorpairs. it contains attribute values like
1376 1376 curses.A_BOLD.
1377 1377 """
1378 1378
1379 1379 if (name is not None) and name in self.colorpairnames:
1380 1380 # then get the associated color pair and return it
1381 1381 colorpair = self.colorpairnames[name]
1382 1382 else:
1383 1383 if fgcolor is None:
1384 1384 fgcolor = -1
1385 1385 if bgcolor is None:
1386 1386 bgcolor = -1
1387 1387 if (fgcolor, bgcolor) in self.colorpairs:
1388 1388 colorpair = self.colorpairs[(fgcolor, bgcolor)]
1389 1389 else:
1390 1390 pairindex = len(self.colorpairs) + 1
1391 1391 if self.usecolor:
1392 1392 curses.init_pair(pairindex, fgcolor, bgcolor)
1393 1393 colorpair = self.colorpairs[(fgcolor, bgcolor)] = (
1394 1394 curses.color_pair(pairindex))
1395 1395 if name is not None:
1396 1396 self.colorpairnames[name] = curses.color_pair(pairindex)
1397 1397 else:
1398 1398 cval = 0
1399 1399 if name is not None:
1400 1400 if name == 'selected':
1401 1401 cval = curses.A_REVERSE
1402 1402 self.colorpairnames[name] = cval
1403 1403 colorpair = self.colorpairs[(fgcolor, bgcolor)] = cval
1404 1404
1405 1405 # add attributes if possible
1406 1406 if attrlist is None:
1407 1407 attrlist = []
1408 1408 if colorpair < 256:
1409 1409 # then it is safe to apply all attributes
1410 1410 for textattr in attrlist:
1411 1411 colorpair |= textattr
1412 1412 else:
1413 1413 # just apply a select few (safe?) attributes
1414 1414 for textattrib in (curses.A_UNDERLINE, curses.A_BOLD):
1415 1415 if textattrib in attrlist:
1416 1416 colorpair |= textattrib
1417 1417 return colorpair
1418 1418
1419 1419 def initcolorpair(self, *args, **kwargs):
1420 1420 "same as getcolorpair."
1421 1421 self.getcolorpair(*args, **kwargs)
1422 1422
1423 1423 def helpwindow(self):
1424 1424 "print a help window to the screen. exit after any keypress."
1425 1425 helptext = _(
1426 1426 """ [press any key to return to the patch-display]
1427 1427
1428 1428 crecord allows you to interactively choose among the changes you have made,
1429 1429 and confirm only those changes you select for further processing by the command
1430 1430 you are running (commit/shelve/revert), after confirming the selected
1431 1431 changes, the unselected changes are still present in your working copy, so you
1432 1432 can use crecord multiple times to split large changes into smaller changesets.
1433 1433 the following are valid keystrokes:
1434 1434
1435 1435 [space] : (un-)select item ([~]/[x] = partly/fully applied)
1436 1436 A : (un-)select all items
1437 1437 up/down-arrow [k/j] : go to previous/next unfolded item
1438 1438 pgup/pgdn [K/J] : go to previous/next item of same type
1439 1439 right/left-arrow [l/h] : go to child item / parent item
1440 1440 shift-left-arrow [H] : go to parent header / fold selected header
1441 1441 f : fold / unfold item, hiding/revealing its children
1442 1442 F : fold / unfold parent item and all of its ancestors
1443 1443 ctrl-l : scroll the selected line to the top of the screen
1444 1444 m : edit / resume editing the commit message
1445 1445 e : edit the currently selected hunk
1446 1446 a : toggle amend mode, only with commit -i
1447 1447 c : confirm selected changes
1448 1448 r : review/edit and confirm selected changes
1449 1449 q : quit without confirming (no changes will be made)
1450 1450 ? : help (what you're currently reading)""")
1451 1451
1452 1452 helpwin = curses.newwin(self.yscreensize, 0, 0, 0)
1453 1453 helplines = helptext.split("\n")
1454 1454 helplines = helplines + [" "]*(
1455 1455 self.yscreensize - self.numstatuslines - len(helplines) - 1)
1456 1456 try:
1457 1457 for line in helplines:
1458 1458 self.printstring(helpwin, line, pairname="legend")
1459 1459 except curses.error:
1460 1460 pass
1461 1461 helpwin.refresh()
1462 1462 try:
1463 1463 with self.ui.timeblockedsection('crecord'):
1464 1464 helpwin.getkey()
1465 1465 except curses.error:
1466 1466 pass
1467 1467
1468 1468 def commitMessageWindow(self):
1469 1469 "Create a temporary commit message editing window on the screen."
1470 1470
1471 1471 curses.raw()
1472 1472 curses.def_prog_mode()
1473 1473 curses.endwin()
1474 1474 self.commenttext = self.ui.edit(self.commenttext, self.ui.username())
1475 1475 curses.cbreak()
1476 1476 self.stdscr.refresh()
1477 1477 self.stdscr.keypad(1) # allow arrow-keys to continue to function
1478 1478
1479 1479 def confirmationwindow(self, windowtext):
1480 1480 "display an informational window, then wait for and return a keypress."
1481 1481
1482 1482 confirmwin = curses.newwin(self.yscreensize, 0, 0, 0)
1483 1483 try:
1484 1484 lines = windowtext.split("\n")
1485 1485 for line in lines:
1486 1486 self.printstring(confirmwin, line, pairname="selected")
1487 1487 except curses.error:
1488 1488 pass
1489 1489 self.stdscr.refresh()
1490 1490 confirmwin.refresh()
1491 1491 try:
1492 1492 with self.ui.timeblockedsection('crecord'):
1493 1493 response = chr(self.stdscr.getch())
1494 1494 except ValueError:
1495 1495 response = None
1496 1496
1497 1497 return response
1498 1498
1499 1499 def reviewcommit(self):
1500 1500 """ask for 'y' to be pressed to confirm selected. return True if
1501 1501 confirmed."""
1502 1502 confirmtext = _(
1503 1503 """if you answer yes to the following, the your currently chosen patch chunks
1504 1504 will be loaded into an editor. you may modify the patch from the editor, and
1505 1505 save the changes if you wish to change the patch. otherwise, you can just
1506 1506 close the editor without saving to accept the current patch as-is.
1507 1507
1508 1508 note: don't add/remove lines unless you also modify the range information.
1509 1509 failing to follow this rule will result in the commit aborting.
1510 1510
1511 1511 are you sure you want to review/edit and confirm the selected changes [yn]?
1512 1512 """)
1513 1513 with self.ui.timeblockedsection('crecord'):
1514 1514 response = self.confirmationwindow(confirmtext)
1515 1515 if response is None:
1516 1516 response = "n"
1517 1517 if response.lower().startswith("y"):
1518 1518 return True
1519 1519 else:
1520 1520 return False
1521 1521
1522 1522 def toggleamend(self, opts, test):
1523 1523 """Toggle the amend flag.
1524 1524
1525 1525 When the amend flag is set, a commit will modify the most recently
1526 1526 committed changeset, instead of creating a new changeset. Otherwise, a
1527 1527 new changeset will be created (the normal commit behavior).
1528 1528 """
1529 1529
1530 1530 try:
1531 1531 ver = float(util.version()[:3])
1532 1532 except ValueError:
1533 1533 ver = 1
1534 1534 if ver < 2.19:
1535 1535 msg = _("The amend option is unavailable with hg versions < 2.2\n\n"
1536 1536 "Press any key to continue.")
1537 1537 elif opts.get('amend') is None:
1538 1538 opts['amend'] = True
1539 1539 msg = _("Amend option is turned on -- committing the currently "
1540 1540 "selected changes will not create a new changeset, but "
1541 1541 "instead update the most recently committed changeset.\n\n"
1542 1542 "Press any key to continue.")
1543 1543 elif opts.get('amend') is True:
1544 1544 opts['amend'] = None
1545 1545 msg = _("Amend option is turned off -- committing the currently "
1546 1546 "selected changes will create a new changeset.\n\n"
1547 1547 "Press any key to continue.")
1548 1548 if not test:
1549 1549 self.confirmationwindow(msg)
1550 1550
1551 1551 def recenterdisplayedarea(self):
1552 1552 """
1553 1553 once we scrolled with pg up pg down we can be pointing outside of the
1554 1554 display zone. we print the patch with towin=False to compute the
1555 1555 location of the selected item even though it is outside of the displayed
1556 1556 zone and then update the scroll.
1557 1557 """
1558 1558 self.printitem(towin=False)
1559 1559 self.updatescroll()
1560 1560
1561 1561 def toggleedit(self, item=None, test=False):
1562 1562 """
1563 1563 edit the currently selected chunk
1564 1564 """
1565 1565 def updateui(self):
1566 1566 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1567 1567 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1568 1568 self.updatescroll()
1569 1569 self.stdscr.refresh()
1570 1570 self.statuswin.refresh()
1571 1571 self.stdscr.keypad(1)
1572 1572
1573 1573 def editpatchwitheditor(self, chunk):
1574 1574 if chunk is None:
1575 1575 self.ui.write(_('cannot edit patch for whole file'))
1576 1576 self.ui.write("\n")
1577 1577 return None
1578 1578 if chunk.header.binary():
1579 1579 self.ui.write(_('cannot edit patch for binary file'))
1580 1580 self.ui.write("\n")
1581 1581 return None
1582 1582
1583 1583 # write the initial patch
1584 1584 patch = stringio()
1585 1585 patch.write(diffhelptext + hunkhelptext)
1586 1586 chunk.header.write(patch)
1587 1587 chunk.write(patch)
1588 1588
1589 1589 # start the editor and wait for it to complete
1590 1590 try:
1591 1591 patch = self.ui.edit(patch.getvalue(), "", action="diff")
1592 1592 except error.Abort as exc:
1593 1593 self.errorstr = str(exc)
1594 1594 return None
1595 1595
1596 1596 # remove comment lines
1597 1597 patch = [line + '\n' for line in patch.splitlines()
1598 1598 if not line.startswith('#')]
1599 1599 return patchmod.parsepatch(patch)
1600 1600
1601 1601 if item is None:
1602 1602 item = self.currentselecteditem
1603 1603 if isinstance(item, uiheader):
1604 1604 return
1605 1605 if isinstance(item, uihunkline):
1606 1606 item = item.parentitem()
1607 1607 if not isinstance(item, uihunk):
1608 1608 return
1609 1609
1610 1610 # To go back to that hunk or its replacement at the end of the edit
1611 1611 itemindex = item.parentitem().hunks.index(item)
1612 1612
1613 1613 beforeadded, beforeremoved = item.added, item.removed
1614 1614 newpatches = editpatchwitheditor(self, item)
1615 1615 if newpatches is None:
1616 1616 if not test:
1617 1617 updateui(self)
1618 1618 return
1619 1619 header = item.header
1620 1620 editedhunkindex = header.hunks.index(item)
1621 1621 hunksbefore = header.hunks[:editedhunkindex]
1622 1622 hunksafter = header.hunks[editedhunkindex + 1:]
1623 1623 newpatchheader = newpatches[0]
1624 1624 newhunks = [uihunk(h, header) for h in newpatchheader.hunks]
1625 1625 newadded = sum([h.added for h in newhunks])
1626 1626 newremoved = sum([h.removed for h in newhunks])
1627 1627 offset = (newadded - beforeadded) - (newremoved - beforeremoved)
1628 1628
1629 1629 for h in hunksafter:
1630 1630 h.toline += offset
1631 1631 for h in newhunks:
1632 1632 h.folded = False
1633 1633 header.hunks = hunksbefore + newhunks + hunksafter
1634 1634 if self.emptypatch():
1635 1635 header.hunks = hunksbefore + [item] + hunksafter
1636 1636 self.currentselecteditem = header
1637 1637 if len(header.hunks) > itemindex:
1638 1638 self.currentselecteditem = header.hunks[itemindex]
1639 1639
1640 1640 if not test:
1641 1641 updateui(self)
1642 1642
1643 1643 def emptypatch(self):
1644 1644 item = self.headerlist
1645 1645 if not item:
1646 1646 return True
1647 1647 for header in item:
1648 1648 if header.hunks:
1649 1649 return False
1650 1650 return True
1651 1651
1652 1652 def handlekeypressed(self, keypressed, test=False):
1653 1653 """
1654 1654 Perform actions based on pressed keys.
1655 1655
1656 1656 Return true to exit the main loop.
1657 1657 """
1658 1658 if keypressed in ["k", "KEY_UP"]:
1659 1659 self.uparrowevent()
1660 1660 if keypressed in ["K", "KEY_PPAGE"]:
1661 1661 self.uparrowshiftevent()
1662 1662 elif keypressed in ["j", "KEY_DOWN"]:
1663 1663 self.downarrowevent()
1664 1664 elif keypressed in ["J", "KEY_NPAGE"]:
1665 1665 self.downarrowshiftevent()
1666 1666 elif keypressed in ["l", "KEY_RIGHT"]:
1667 1667 self.rightarrowevent()
1668 1668 elif keypressed in ["h", "KEY_LEFT"]:
1669 1669 self.leftarrowevent()
1670 1670 elif keypressed in ["H", "KEY_SLEFT"]:
1671 1671 self.leftarrowshiftevent()
1672 1672 elif keypressed in ["q"]:
1673 1673 raise error.Abort(_('user quit'))
1674 1674 elif keypressed in ['a']:
1675 1675 self.toggleamend(self.opts, test)
1676 1676 elif keypressed in ["c"]:
1677 1677 return True
1678 1678 elif test and keypressed in ['X']:
1679 1679 return True
1680 1680 elif keypressed in ["r"]:
1681 1681 if self.reviewcommit():
1682 1682 self.opts['review'] = True
1683 1683 return True
1684 1684 elif test and keypressed in ['R']:
1685 1685 self.opts['review'] = True
1686 1686 return True
1687 1687 elif keypressed in [' '] or (test and keypressed in ["TOGGLE"]):
1688 1688 self.toggleapply()
1689 1689 if self.ui.configbool('experimental', 'spacemovesdown'):
1690 1690 self.downarrowevent()
1691 1691 elif keypressed in ['A']:
1692 1692 self.toggleall()
1693 1693 elif keypressed in ['e']:
1694 1694 self.toggleedit(test=test)
1695 1695 elif keypressed in ["f"]:
1696 1696 self.togglefolded()
1697 1697 elif keypressed in ["F"]:
1698 1698 self.togglefolded(foldparent=True)
1699 1699 elif keypressed in ["m"]:
1700 1700 self.commitMessageWindow()
1701 1701 elif keypressed in ["?"]:
1702 1702 self.helpwindow()
1703 1703 self.stdscr.clear()
1704 1704 self.stdscr.refresh()
1705 1705 elif curses.unctrl(keypressed) in ["^L"]:
1706 1706 # scroll the current line to the top of the screen
1707 1707 self.scrolllines(self.selecteditemstartline)
1708 1708
1709 1709 def main(self, stdscr):
1710 1710 """
1711 1711 method to be wrapped by curses.wrapper() for selecting chunks.
1712 1712 """
1713 1713
1714 1714 origsigwinch = sentinel = object()
1715 1715 if util.safehasattr(signal, 'SIGWINCH'):
1716 1716 origsigwinch = signal.signal(signal.SIGWINCH,
1717 1717 self.sigwinchhandler)
1718 1718 try:
1719 1719 return self._main(stdscr)
1720 1720 finally:
1721 1721 if origsigwinch is not sentinel:
1722 1722 signal.signal(signal.SIGWINCH, origsigwinch)
1723 1723
1724 1724 def _main(self, stdscr):
1725 1725 self.stdscr = stdscr
1726 1726 # error during initialization, cannot be printed in the curses
1727 1727 # interface, it should be printed by the calling code
1728 1728 self.initexc = None
1729 1729 self.yscreensize, self.xscreensize = self.stdscr.getmaxyx()
1730 1730
1731 1731 curses.start_color()
1732 1732 try:
1733 1733 curses.use_default_colors()
1734 1734 except curses.error:
1735 1735 self.usecolor = False
1736 1736
1737 1737 # available colors: black, blue, cyan, green, magenta, white, yellow
1738 1738 # init_pair(color_id, foreground_color, background_color)
1739 1739 self.initcolorpair(None, None, name="normal")
1740 1740 self.initcolorpair(curses.COLOR_WHITE, curses.COLOR_MAGENTA,
1741 1741 name="selected")
1742 1742 self.initcolorpair(curses.COLOR_RED, None, name="deletion")
1743 1743 self.initcolorpair(curses.COLOR_GREEN, None, name="addition")
1744 1744 self.initcolorpair(curses.COLOR_WHITE, curses.COLOR_BLUE, name="legend")
1745 1745 # newwin([height, width,] begin_y, begin_x)
1746 1746 self.statuswin = curses.newwin(self.numstatuslines, 0, 0, 0)
1747 1747 self.statuswin.keypad(1) # interpret arrow-key, etc. esc sequences
1748 1748
1749 1749 # figure out how much space to allocate for the chunk-pad which is
1750 1750 # used for displaying the patch
1751 1751
1752 1752 # stupid hack to prevent getnumlinesdisplayed from failing
1753 1753 self.chunkpad = curses.newpad(1, self.xscreensize)
1754 1754
1755 1755 # add 1 so to account for last line text reaching end of line
1756 1756 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1757 1757
1758 1758 try:
1759 1759 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1760 1760 except curses.error:
1761 1761 self.initexc = fallbackerror(
1762 1762 _('this diff is too large to be displayed'))
1763 1763 return
1764 1764 # initialize selecteditemendline (initial start-line is 0)
1765 1765 self.selecteditemendline = self.getnumlinesdisplayed(
1766 1766 self.currentselecteditem, recursechildren=False)
1767 1767
1768 1768 while True:
1769 1769 self.updatescreen()
1770 1770 try:
1771 1771 with self.ui.timeblockedsection('crecord'):
1772 1772 keypressed = self.statuswin.getkey()
1773 1773 if self.errorstr is not None:
1774 1774 self.errorstr = None
1775 1775 continue
1776 1776 except curses.error:
1777 1777 keypressed = "foobar"
1778 1778 if self.handlekeypressed(keypressed):
1779 1779 break
1780 1780
1781 1781 if self.commenttext != "":
1782 1782 whitespaceremoved = re.sub("(?m)^\s.*(\n|$)", "", self.commenttext)
1783 1783 if whitespaceremoved != "":
1784 1784 self.opts['message'] = self.commenttext
@@ -1,2232 +1,2234 b''
1 1 # merge.py - directory-level update/merge handling for Mercurial
2 2 #
3 3 # Copyright 2006, 2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import errno
11 11 import hashlib
12 12 import shutil
13 13 import struct
14 14
15 15 from .i18n import _
16 16 from .node import (
17 17 addednodeid,
18 18 bin,
19 19 hex,
20 20 modifiednodeid,
21 21 nullhex,
22 22 nullid,
23 23 nullrev,
24 24 )
25 25 from .thirdparty import (
26 26 attr,
27 27 )
28 28 from . import (
29 29 copies,
30 30 error,
31 31 filemerge,
32 32 match as matchmod,
33 33 obsutil,
34 34 pycompat,
35 35 scmutil,
36 36 subrepoutil,
37 37 util,
38 38 worker,
39 39 )
40 40
41 41 _pack = struct.pack
42 42 _unpack = struct.unpack
43 43
44 44 def _droponode(data):
45 45 # used for compatibility for v1
46 46 bits = data.split('\0')
47 47 bits = bits[:-2] + bits[-1:]
48 48 return '\0'.join(bits)
49 49
50 50 # Merge state record types. See ``mergestate`` docs for more.
51 51 RECORD_LOCAL = b'L'
52 52 RECORD_OTHER = b'O'
53 53 RECORD_MERGED = b'F'
54 54 RECORD_CHANGEDELETE_CONFLICT = b'C'
55 55 RECORD_MERGE_DRIVER_MERGE = b'D'
56 56 RECORD_PATH_CONFLICT = b'P'
57 57 RECORD_MERGE_DRIVER_STATE = b'm'
58 58 RECORD_FILE_VALUES = b'f'
59 59 RECORD_LABELS = b'l'
60 60 RECORD_OVERRIDE = b't'
61 61 RECORD_UNSUPPORTED_MANDATORY = b'X'
62 62 RECORD_UNSUPPORTED_ADVISORY = b'x'
63 63
64 64 MERGE_DRIVER_STATE_UNMARKED = b'u'
65 65 MERGE_DRIVER_STATE_MARKED = b'm'
66 66 MERGE_DRIVER_STATE_SUCCESS = b's'
67 67
68 68 MERGE_RECORD_UNRESOLVED = b'u'
69 69 MERGE_RECORD_RESOLVED = b'r'
70 70 MERGE_RECORD_UNRESOLVED_PATH = b'pu'
71 71 MERGE_RECORD_RESOLVED_PATH = b'pr'
72 72 MERGE_RECORD_DRIVER_RESOLVED = b'd'
73 73
74 74 ACTION_FORGET = b'f'
75 75 ACTION_REMOVE = b'r'
76 76 ACTION_ADD = b'a'
77 77 ACTION_GET = b'g'
78 78 ACTION_PATH_CONFLICT = b'p'
79 79 ACTION_PATH_CONFLICT_RESOLVE = b'pr'
80 80 ACTION_ADD_MODIFIED = b'am'
81 81 ACTION_CREATED = b'c'
82 82 ACTION_DELETED_CHANGED = b'dc'
83 83 ACTION_CHANGED_DELETED = b'cd'
84 84 ACTION_MERGE = b'm'
85 85 ACTION_LOCAL_DIR_RENAME_GET = b'dg'
86 86 ACTION_DIR_RENAME_MOVE_LOCAL = b'dm'
87 87 ACTION_KEEP = b'k'
88 88 ACTION_EXEC = b'e'
89 89 ACTION_CREATED_MERGE = b'cm'
90 90
91 91 class mergestate(object):
92 92 '''track 3-way merge state of individual files
93 93
94 94 The merge state is stored on disk when needed. Two files are used: one with
95 95 an old format (version 1), and one with a new format (version 2). Version 2
96 96 stores a superset of the data in version 1, including new kinds of records
97 97 in the future. For more about the new format, see the documentation for
98 98 `_readrecordsv2`.
99 99
100 100 Each record can contain arbitrary content, and has an associated type. This
101 101 `type` should be a letter. If `type` is uppercase, the record is mandatory:
102 102 versions of Mercurial that don't support it should abort. If `type` is
103 103 lowercase, the record can be safely ignored.
104 104
105 105 Currently known records:
106 106
107 107 L: the node of the "local" part of the merge (hexified version)
108 108 O: the node of the "other" part of the merge (hexified version)
109 109 F: a file to be merged entry
110 110 C: a change/delete or delete/change conflict
111 111 D: a file that the external merge driver will merge internally
112 112 (experimental)
113 113 P: a path conflict (file vs directory)
114 114 m: the external merge driver defined for this merge plus its run state
115 115 (experimental)
116 116 f: a (filename, dictionary) tuple of optional values for a given file
117 117 X: unsupported mandatory record type (used in tests)
118 118 x: unsupported advisory record type (used in tests)
119 119 l: the labels for the parts of the merge.
120 120
121 121 Merge driver run states (experimental):
122 122 u: driver-resolved files unmarked -- needs to be run next time we're about
123 123 to resolve or commit
124 124 m: driver-resolved files marked -- only needs to be run before commit
125 125 s: success/skipped -- does not need to be run any more
126 126
127 127 Merge record states (stored in self._state, indexed by filename):
128 128 u: unresolved conflict
129 129 r: resolved conflict
130 130 pu: unresolved path conflict (file conflicts with directory)
131 131 pr: resolved path conflict
132 132 d: driver-resolved conflict
133 133
134 134 The resolve command transitions between 'u' and 'r' for conflicts and
135 135 'pu' and 'pr' for path conflicts.
136 136 '''
137 137 statepathv1 = 'merge/state'
138 138 statepathv2 = 'merge/state2'
139 139
140 140 @staticmethod
141 141 def clean(repo, node=None, other=None, labels=None):
142 142 """Initialize a brand new merge state, removing any existing state on
143 143 disk."""
144 144 ms = mergestate(repo)
145 145 ms.reset(node, other, labels)
146 146 return ms
147 147
148 148 @staticmethod
149 149 def read(repo):
150 150 """Initialize the merge state, reading it from disk."""
151 151 ms = mergestate(repo)
152 152 ms._read()
153 153 return ms
154 154
155 155 def __init__(self, repo):
156 156 """Initialize the merge state.
157 157
158 158 Do not use this directly! Instead call read() or clean()."""
159 159 self._repo = repo
160 160 self._dirty = False
161 161 self._labels = None
162 162
163 163 def reset(self, node=None, other=None, labels=None):
164 164 self._state = {}
165 165 self._stateextras = {}
166 166 self._local = None
167 167 self._other = None
168 168 self._labels = labels
169 169 for var in ('localctx', 'otherctx'):
170 170 if var in vars(self):
171 171 delattr(self, var)
172 172 if node:
173 173 self._local = node
174 174 self._other = other
175 175 self._readmergedriver = None
176 176 if self.mergedriver:
177 177 self._mdstate = MERGE_DRIVER_STATE_SUCCESS
178 178 else:
179 179 self._mdstate = MERGE_DRIVER_STATE_UNMARKED
180 180 shutil.rmtree(self._repo.vfs.join('merge'), True)
181 181 self._results = {}
182 182 self._dirty = False
183 183
184 184 def _read(self):
185 185 """Analyse each record content to restore a serialized state from disk
186 186
187 187 This function process "record" entry produced by the de-serialization
188 188 of on disk file.
189 189 """
190 190 self._state = {}
191 191 self._stateextras = {}
192 192 self._local = None
193 193 self._other = None
194 194 for var in ('localctx', 'otherctx'):
195 195 if var in vars(self):
196 196 delattr(self, var)
197 197 self._readmergedriver = None
198 198 self._mdstate = MERGE_DRIVER_STATE_SUCCESS
199 199 unsupported = set()
200 200 records = self._readrecords()
201 201 for rtype, record in records:
202 202 if rtype == RECORD_LOCAL:
203 203 self._local = bin(record)
204 204 elif rtype == RECORD_OTHER:
205 205 self._other = bin(record)
206 206 elif rtype == RECORD_MERGE_DRIVER_STATE:
207 207 bits = record.split('\0', 1)
208 208 mdstate = bits[1]
209 209 if len(mdstate) != 1 or mdstate not in (
210 210 MERGE_DRIVER_STATE_UNMARKED, MERGE_DRIVER_STATE_MARKED,
211 211 MERGE_DRIVER_STATE_SUCCESS):
212 212 # the merge driver should be idempotent, so just rerun it
213 213 mdstate = MERGE_DRIVER_STATE_UNMARKED
214 214
215 215 self._readmergedriver = bits[0]
216 216 self._mdstate = mdstate
217 217 elif rtype in (RECORD_MERGED, RECORD_CHANGEDELETE_CONFLICT,
218 218 RECORD_PATH_CONFLICT, RECORD_MERGE_DRIVER_MERGE):
219 219 bits = record.split('\0')
220 220 self._state[bits[0]] = bits[1:]
221 221 elif rtype == RECORD_FILE_VALUES:
222 222 filename, rawextras = record.split('\0', 1)
223 223 extraparts = rawextras.split('\0')
224 224 extras = {}
225 225 i = 0
226 226 while i < len(extraparts):
227 227 extras[extraparts[i]] = extraparts[i + 1]
228 228 i += 2
229 229
230 230 self._stateextras[filename] = extras
231 231 elif rtype == RECORD_LABELS:
232 232 labels = record.split('\0', 2)
233 233 self._labels = [l for l in labels if len(l) > 0]
234 234 elif not rtype.islower():
235 235 unsupported.add(rtype)
236 236 self._results = {}
237 237 self._dirty = False
238 238
239 239 if unsupported:
240 240 raise error.UnsupportedMergeRecords(unsupported)
241 241
242 242 def _readrecords(self):
243 243 """Read merge state from disk and return a list of record (TYPE, data)
244 244
245 245 We read data from both v1 and v2 files and decide which one to use.
246 246
247 247 V1 has been used by version prior to 2.9.1 and contains less data than
248 248 v2. We read both versions and check if no data in v2 contradicts
249 249 v1. If there is not contradiction we can safely assume that both v1
250 250 and v2 were written at the same time and use the extract data in v2. If
251 251 there is contradiction we ignore v2 content as we assume an old version
252 252 of Mercurial has overwritten the mergestate file and left an old v2
253 253 file around.
254 254
255 255 returns list of record [(TYPE, data), ...]"""
256 256 v1records = self._readrecordsv1()
257 257 v2records = self._readrecordsv2()
258 258 if self._v1v2match(v1records, v2records):
259 259 return v2records
260 260 else:
261 261 # v1 file is newer than v2 file, use it
262 262 # we have to infer the "other" changeset of the merge
263 263 # we cannot do better than that with v1 of the format
264 264 mctx = self._repo[None].parents()[-1]
265 265 v1records.append((RECORD_OTHER, mctx.hex()))
266 266 # add place holder "other" file node information
267 267 # nobody is using it yet so we do no need to fetch the data
268 268 # if mctx was wrong `mctx[bits[-2]]` may fails.
269 269 for idx, r in enumerate(v1records):
270 270 if r[0] == RECORD_MERGED:
271 271 bits = r[1].split('\0')
272 272 bits.insert(-2, '')
273 273 v1records[idx] = (r[0], '\0'.join(bits))
274 274 return v1records
275 275
276 276 def _v1v2match(self, v1records, v2records):
277 277 oldv2 = set() # old format version of v2 record
278 278 for rec in v2records:
279 279 if rec[0] == RECORD_LOCAL:
280 280 oldv2.add(rec)
281 281 elif rec[0] == RECORD_MERGED:
282 282 # drop the onode data (not contained in v1)
283 283 oldv2.add((RECORD_MERGED, _droponode(rec[1])))
284 284 for rec in v1records:
285 285 if rec not in oldv2:
286 286 return False
287 287 else:
288 288 return True
289 289
290 290 def _readrecordsv1(self):
291 291 """read on disk merge state for version 1 file
292 292
293 293 returns list of record [(TYPE, data), ...]
294 294
295 295 Note: the "F" data from this file are one entry short
296 296 (no "other file node" entry)
297 297 """
298 298 records = []
299 299 try:
300 300 f = self._repo.vfs(self.statepathv1)
301 301 for i, l in enumerate(f):
302 302 if i == 0:
303 303 records.append((RECORD_LOCAL, l[:-1]))
304 304 else:
305 305 records.append((RECORD_MERGED, l[:-1]))
306 306 f.close()
307 307 except IOError as err:
308 308 if err.errno != errno.ENOENT:
309 309 raise
310 310 return records
311 311
312 312 def _readrecordsv2(self):
313 313 """read on disk merge state for version 2 file
314 314
315 315 This format is a list of arbitrary records of the form:
316 316
317 317 [type][length][content]
318 318
319 319 `type` is a single character, `length` is a 4 byte integer, and
320 320 `content` is an arbitrary byte sequence of length `length`.
321 321
322 322 Mercurial versions prior to 3.7 have a bug where if there are
323 323 unsupported mandatory merge records, attempting to clear out the merge
324 324 state with hg update --clean or similar aborts. The 't' record type
325 325 works around that by writing out what those versions treat as an
326 326 advisory record, but later versions interpret as special: the first
327 327 character is the 'real' record type and everything onwards is the data.
328 328
329 329 Returns list of records [(TYPE, data), ...]."""
330 330 records = []
331 331 try:
332 332 f = self._repo.vfs(self.statepathv2)
333 333 data = f.read()
334 334 off = 0
335 335 end = len(data)
336 336 while off < end:
337 337 rtype = data[off:off + 1]
338 338 off += 1
339 339 length = _unpack('>I', data[off:(off + 4)])[0]
340 340 off += 4
341 341 record = data[off:(off + length)]
342 342 off += length
343 343 if rtype == RECORD_OVERRIDE:
344 344 rtype, record = record[0:1], record[1:]
345 345 records.append((rtype, record))
346 346 f.close()
347 347 except IOError as err:
348 348 if err.errno != errno.ENOENT:
349 349 raise
350 350 return records
351 351
352 352 @util.propertycache
353 353 def mergedriver(self):
354 354 # protect against the following:
355 355 # - A configures a malicious merge driver in their hgrc, then
356 356 # pauses the merge
357 357 # - A edits their hgrc to remove references to the merge driver
358 358 # - A gives a copy of their entire repo, including .hg, to B
359 359 # - B inspects .hgrc and finds it to be clean
360 360 # - B then continues the merge and the malicious merge driver
361 361 # gets invoked
362 362 configmergedriver = self._repo.ui.config('experimental', 'mergedriver')
363 363 if (self._readmergedriver is not None
364 364 and self._readmergedriver != configmergedriver):
365 365 raise error.ConfigError(
366 366 _("merge driver changed since merge started"),
367 367 hint=_("revert merge driver change or abort merge"))
368 368
369 369 return configmergedriver
370 370
371 371 @util.propertycache
372 372 def localctx(self):
373 373 if self._local is None:
374 374 msg = "localctx accessed but self._local isn't set"
375 375 raise error.ProgrammingError(msg)
376 376 return self._repo[self._local]
377 377
378 378 @util.propertycache
379 379 def otherctx(self):
380 380 if self._other is None:
381 381 msg = "otherctx accessed but self._other isn't set"
382 382 raise error.ProgrammingError(msg)
383 383 return self._repo[self._other]
384 384
385 385 def active(self):
386 386 """Whether mergestate is active.
387 387
388 388 Returns True if there appears to be mergestate. This is a rough proxy
389 389 for "is a merge in progress."
390 390 """
391 391 # Check local variables before looking at filesystem for performance
392 392 # reasons.
393 393 return bool(self._local) or bool(self._state) or \
394 394 self._repo.vfs.exists(self.statepathv1) or \
395 395 self._repo.vfs.exists(self.statepathv2)
396 396
397 397 def commit(self):
398 398 """Write current state on disk (if necessary)"""
399 399 if self._dirty:
400 400 records = self._makerecords()
401 401 self._writerecords(records)
402 402 self._dirty = False
403 403
404 404 def _makerecords(self):
405 405 records = []
406 406 records.append((RECORD_LOCAL, hex(self._local)))
407 407 records.append((RECORD_OTHER, hex(self._other)))
408 408 if self.mergedriver:
409 409 records.append((RECORD_MERGE_DRIVER_STATE, '\0'.join([
410 410 self.mergedriver, self._mdstate])))
411 411 # Write out state items. In all cases, the value of the state map entry
412 412 # is written as the contents of the record. The record type depends on
413 413 # the type of state that is stored, and capital-letter records are used
414 414 # to prevent older versions of Mercurial that do not support the feature
415 415 # from loading them.
416 416 for filename, v in self._state.iteritems():
417 417 if v[0] == MERGE_RECORD_DRIVER_RESOLVED:
418 418 # Driver-resolved merge. These are stored in 'D' records.
419 419 records.append((RECORD_MERGE_DRIVER_MERGE,
420 420 '\0'.join([filename] + v)))
421 421 elif v[0] in (MERGE_RECORD_UNRESOLVED_PATH,
422 422 MERGE_RECORD_RESOLVED_PATH):
423 423 # Path conflicts. These are stored in 'P' records. The current
424 424 # resolution state ('pu' or 'pr') is stored within the record.
425 425 records.append((RECORD_PATH_CONFLICT,
426 426 '\0'.join([filename] + v)))
427 427 elif v[1] == nullhex or v[6] == nullhex:
428 428 # Change/Delete or Delete/Change conflicts. These are stored in
429 429 # 'C' records. v[1] is the local file, and is nullhex when the
430 430 # file is deleted locally ('dc'). v[6] is the remote file, and
431 431 # is nullhex when the file is deleted remotely ('cd').
432 432 records.append((RECORD_CHANGEDELETE_CONFLICT,
433 433 '\0'.join([filename] + v)))
434 434 else:
435 435 # Normal files. These are stored in 'F' records.
436 436 records.append((RECORD_MERGED,
437 437 '\0'.join([filename] + v)))
438 438 for filename, extras in sorted(self._stateextras.iteritems()):
439 439 rawextras = '\0'.join('%s\0%s' % (k, v) for k, v in
440 440 extras.iteritems())
441 441 records.append((RECORD_FILE_VALUES,
442 442 '%s\0%s' % (filename, rawextras)))
443 443 if self._labels is not None:
444 444 labels = '\0'.join(self._labels)
445 445 records.append((RECORD_LABELS, labels))
446 446 return records
447 447
448 448 def _writerecords(self, records):
449 449 """Write current state on disk (both v1 and v2)"""
450 450 self._writerecordsv1(records)
451 451 self._writerecordsv2(records)
452 452
453 453 def _writerecordsv1(self, records):
454 454 """Write current state on disk in a version 1 file"""
455 455 f = self._repo.vfs(self.statepathv1, 'wb')
456 456 irecords = iter(records)
457 457 lrecords = next(irecords)
458 458 assert lrecords[0] == RECORD_LOCAL
459 459 f.write(hex(self._local) + '\n')
460 460 for rtype, data in irecords:
461 461 if rtype == RECORD_MERGED:
462 462 f.write('%s\n' % _droponode(data))
463 463 f.close()
464 464
465 465 def _writerecordsv2(self, records):
466 466 """Write current state on disk in a version 2 file
467 467
468 468 See the docstring for _readrecordsv2 for why we use 't'."""
469 469 # these are the records that all version 2 clients can read
470 470 allowlist = (RECORD_LOCAL, RECORD_OTHER, RECORD_MERGED)
471 471 f = self._repo.vfs(self.statepathv2, 'wb')
472 472 for key, data in records:
473 473 assert len(key) == 1
474 474 if key not in allowlist:
475 475 key, data = RECORD_OVERRIDE, '%s%s' % (key, data)
476 476 format = '>sI%is' % len(data)
477 477 f.write(_pack(format, key, len(data), data))
478 478 f.close()
479 479
480 480 def add(self, fcl, fco, fca, fd):
481 481 """add a new (potentially?) conflicting file the merge state
482 482 fcl: file context for local,
483 483 fco: file context for remote,
484 484 fca: file context for ancestors,
485 485 fd: file path of the resulting merge.
486 486
487 487 note: also write the local version to the `.hg/merge` directory.
488 488 """
489 489 if fcl.isabsent():
490 490 hash = nullhex
491 491 else:
492 492 hash = hex(hashlib.sha1(fcl.path()).digest())
493 493 self._repo.vfs.write('merge/' + hash, fcl.data())
494 494 self._state[fd] = [MERGE_RECORD_UNRESOLVED, hash, fcl.path(),
495 495 fca.path(), hex(fca.filenode()),
496 496 fco.path(), hex(fco.filenode()),
497 497 fcl.flags()]
498 498 self._stateextras[fd] = {'ancestorlinknode': hex(fca.node())}
499 499 self._dirty = True
500 500
501 501 def addpath(self, path, frename, forigin):
502 502 """add a new conflicting path to the merge state
503 503 path: the path that conflicts
504 504 frename: the filename the conflicting file was renamed to
505 505 forigin: origin of the file ('l' or 'r' for local/remote)
506 506 """
507 507 self._state[path] = [MERGE_RECORD_UNRESOLVED_PATH, frename, forigin]
508 508 self._dirty = True
509 509
510 510 def __contains__(self, dfile):
511 511 return dfile in self._state
512 512
513 513 def __getitem__(self, dfile):
514 514 return self._state[dfile][0]
515 515
516 516 def __iter__(self):
517 517 return iter(sorted(self._state))
518 518
519 519 def files(self):
520 520 return self._state.keys()
521 521
522 522 def mark(self, dfile, state):
523 523 self._state[dfile][0] = state
524 524 self._dirty = True
525 525
526 526 def mdstate(self):
527 527 return self._mdstate
528 528
529 529 def unresolved(self):
530 530 """Obtain the paths of unresolved files."""
531 531
532 532 for f, entry in self._state.iteritems():
533 533 if entry[0] in (MERGE_RECORD_UNRESOLVED,
534 534 MERGE_RECORD_UNRESOLVED_PATH):
535 535 yield f
536 536
537 537 def driverresolved(self):
538 538 """Obtain the paths of driver-resolved files."""
539 539
540 540 for f, entry in self._state.items():
541 541 if entry[0] == MERGE_RECORD_DRIVER_RESOLVED:
542 542 yield f
543 543
544 544 def extras(self, filename):
545 545 return self._stateextras.setdefault(filename, {})
546 546
547 547 def _resolve(self, preresolve, dfile, wctx):
548 548 """rerun merge process for file path `dfile`"""
549 549 if self[dfile] in (MERGE_RECORD_RESOLVED,
550 550 MERGE_RECORD_DRIVER_RESOLVED):
551 551 return True, 0
552 552 stateentry = self._state[dfile]
553 553 state, hash, lfile, afile, anode, ofile, onode, flags = stateentry
554 554 octx = self._repo[self._other]
555 555 extras = self.extras(dfile)
556 556 anccommitnode = extras.get('ancestorlinknode')
557 557 if anccommitnode:
558 558 actx = self._repo[anccommitnode]
559 559 else:
560 560 actx = None
561 561 fcd = self._filectxorabsent(hash, wctx, dfile)
562 562 fco = self._filectxorabsent(onode, octx, ofile)
563 563 # TODO: move this to filectxorabsent
564 564 fca = self._repo.filectx(afile, fileid=anode, changectx=actx)
565 565 # "premerge" x flags
566 566 flo = fco.flags()
567 567 fla = fca.flags()
568 568 if 'x' in flags + flo + fla and 'l' not in flags + flo + fla:
569 569 if fca.node() == nullid and flags != flo:
570 570 if preresolve:
571 571 self._repo.ui.warn(
572 572 _('warning: cannot merge flags for %s '
573 573 'without common ancestor - keeping local flags\n')
574 574 % afile)
575 575 elif flags == fla:
576 576 flags = flo
577 577 if preresolve:
578 578 # restore local
579 579 if hash != nullhex:
580 580 f = self._repo.vfs('merge/' + hash)
581 581 wctx[dfile].write(f.read(), flags)
582 582 f.close()
583 583 else:
584 584 wctx[dfile].remove(ignoremissing=True)
585 585 complete, r, deleted = filemerge.premerge(self._repo, wctx,
586 586 self._local, lfile, fcd,
587 587 fco, fca,
588 588 labels=self._labels)
589 589 else:
590 590 complete, r, deleted = filemerge.filemerge(self._repo, wctx,
591 591 self._local, lfile, fcd,
592 592 fco, fca,
593 593 labels=self._labels)
594 594 if r is None:
595 595 # no real conflict
596 596 del self._state[dfile]
597 597 self._stateextras.pop(dfile, None)
598 598 self._dirty = True
599 599 elif not r:
600 600 self.mark(dfile, MERGE_RECORD_RESOLVED)
601 601
602 602 if complete:
603 603 action = None
604 604 if deleted:
605 605 if fcd.isabsent():
606 606 # dc: local picked. Need to drop if present, which may
607 607 # happen on re-resolves.
608 608 action = ACTION_FORGET
609 609 else:
610 610 # cd: remote picked (or otherwise deleted)
611 611 action = ACTION_REMOVE
612 612 else:
613 613 if fcd.isabsent(): # dc: remote picked
614 614 action = ACTION_GET
615 615 elif fco.isabsent(): # cd: local picked
616 616 if dfile in self.localctx:
617 617 action = ACTION_ADD_MODIFIED
618 618 else:
619 619 action = ACTION_ADD
620 620 # else: regular merges (no action necessary)
621 621 self._results[dfile] = r, action
622 622
623 623 return complete, r
624 624
625 625 def _filectxorabsent(self, hexnode, ctx, f):
626 626 if hexnode == nullhex:
627 627 return filemerge.absentfilectx(ctx, f)
628 628 else:
629 629 return ctx[f]
630 630
631 631 def preresolve(self, dfile, wctx):
632 632 """run premerge process for dfile
633 633
634 634 Returns whether the merge is complete, and the exit code."""
635 635 return self._resolve(True, dfile, wctx)
636 636
637 637 def resolve(self, dfile, wctx):
638 638 """run merge process (assuming premerge was run) for dfile
639 639
640 640 Returns the exit code of the merge."""
641 641 return self._resolve(False, dfile, wctx)[1]
642 642
643 643 def counts(self):
644 644 """return counts for updated, merged and removed files in this
645 645 session"""
646 646 updated, merged, removed = 0, 0, 0
647 647 for r, action in self._results.itervalues():
648 648 if r is None:
649 649 updated += 1
650 650 elif r == 0:
651 651 if action == ACTION_REMOVE:
652 652 removed += 1
653 653 else:
654 654 merged += 1
655 655 return updated, merged, removed
656 656
657 657 def unresolvedcount(self):
658 658 """get unresolved count for this merge (persistent)"""
659 659 return len(list(self.unresolved()))
660 660
661 661 def actions(self):
662 662 """return lists of actions to perform on the dirstate"""
663 663 actions = {
664 664 ACTION_REMOVE: [],
665 665 ACTION_FORGET: [],
666 666 ACTION_ADD: [],
667 667 ACTION_ADD_MODIFIED: [],
668 668 ACTION_GET: [],
669 669 }
670 670 for f, (r, action) in self._results.iteritems():
671 671 if action is not None:
672 672 actions[action].append((f, None, "merge result"))
673 673 return actions
674 674
675 675 def recordactions(self):
676 676 """record remove/add/get actions in the dirstate"""
677 677 branchmerge = self._repo.dirstate.p2() != nullid
678 678 recordupdates(self._repo, self.actions(), branchmerge)
679 679
680 680 def queueremove(self, f):
681 681 """queues a file to be removed from the dirstate
682 682
683 683 Meant for use by custom merge drivers."""
684 684 self._results[f] = 0, ACTION_REMOVE
685 685
686 686 def queueadd(self, f):
687 687 """queues a file to be added to the dirstate
688 688
689 689 Meant for use by custom merge drivers."""
690 690 self._results[f] = 0, ACTION_ADD
691 691
692 692 def queueget(self, f):
693 693 """queues a file to be marked modified in the dirstate
694 694
695 695 Meant for use by custom merge drivers."""
696 696 self._results[f] = 0, ACTION_GET
697 697
698 698 def _getcheckunknownconfig(repo, section, name):
699 699 config = repo.ui.config(section, name)
700 700 valid = ['abort', 'ignore', 'warn']
701 701 if config not in valid:
702 702 validstr = ', '.join(["'" + v + "'" for v in valid])
703 703 raise error.ConfigError(_("%s.%s not valid "
704 704 "('%s' is none of %s)")
705 705 % (section, name, config, validstr))
706 706 return config
707 707
708 708 def _checkunknownfile(repo, wctx, mctx, f, f2=None):
709 709 if wctx.isinmemory():
710 710 # Nothing to do in IMM because nothing in the "working copy" can be an
711 711 # unknown file.
712 712 #
713 713 # Note that we should bail out here, not in ``_checkunknownfiles()``,
714 714 # because that function does other useful work.
715 715 return False
716 716
717 717 if f2 is None:
718 718 f2 = f
719 719 return (repo.wvfs.audit.check(f)
720 720 and repo.wvfs.isfileorlink(f)
721 721 and repo.dirstate.normalize(f) not in repo.dirstate
722 722 and mctx[f2].cmp(wctx[f]))
723 723
724 724 class _unknowndirschecker(object):
725 725 """
726 726 Look for any unknown files or directories that may have a path conflict
727 727 with a file. If any path prefix of the file exists as a file or link,
728 728 then it conflicts. If the file itself is a directory that contains any
729 729 file that is not tracked, then it conflicts.
730 730
731 731 Returns the shortest path at which a conflict occurs, or None if there is
732 732 no conflict.
733 733 """
734 734 def __init__(self):
735 735 # A set of paths known to be good. This prevents repeated checking of
736 736 # dirs. It will be updated with any new dirs that are checked and found
737 737 # to be safe.
738 738 self._unknowndircache = set()
739 739
740 740 # A set of paths that are known to be absent. This prevents repeated
741 741 # checking of subdirectories that are known not to exist. It will be
742 742 # updated with any new dirs that are checked and found to be absent.
743 743 self._missingdircache = set()
744 744
745 745 def __call__(self, repo, wctx, f):
746 746 if wctx.isinmemory():
747 747 # Nothing to do in IMM for the same reason as ``_checkunknownfile``.
748 748 return False
749 749
750 750 # Check for path prefixes that exist as unknown files.
751 751 for p in reversed(list(util.finddirs(f))):
752 752 if p in self._missingdircache:
753 753 return
754 754 if p in self._unknowndircache:
755 755 continue
756 756 if repo.wvfs.audit.check(p):
757 757 if (repo.wvfs.isfileorlink(p)
758 758 and repo.dirstate.normalize(p) not in repo.dirstate):
759 759 return p
760 760 if not repo.wvfs.lexists(p):
761 761 self._missingdircache.add(p)
762 762 return
763 763 self._unknowndircache.add(p)
764 764
765 765 # Check if the file conflicts with a directory containing unknown files.
766 766 if repo.wvfs.audit.check(f) and repo.wvfs.isdir(f):
767 767 # Does the directory contain any files that are not in the dirstate?
768 768 for p, dirs, files in repo.wvfs.walk(f):
769 769 for fn in files:
770 770 relf = util.pconvert(repo.wvfs.reljoin(p, fn))
771 771 relf = repo.dirstate.normalize(relf, isknown=True)
772 772 if relf not in repo.dirstate:
773 773 return f
774 774 return None
775 775
776 776 def _checkunknownfiles(repo, wctx, mctx, force, actions, mergeforce):
777 777 """
778 778 Considers any actions that care about the presence of conflicting unknown
779 779 files. For some actions, the result is to abort; for others, it is to
780 780 choose a different action.
781 781 """
782 782 fileconflicts = set()
783 783 pathconflicts = set()
784 784 warnconflicts = set()
785 785 abortconflicts = set()
786 786 unknownconfig = _getcheckunknownconfig(repo, 'merge', 'checkunknown')
787 787 ignoredconfig = _getcheckunknownconfig(repo, 'merge', 'checkignored')
788 788 pathconfig = repo.ui.configbool('experimental', 'merge.checkpathconflicts')
789 789 if not force:
790 790 def collectconflicts(conflicts, config):
791 791 if config == 'abort':
792 792 abortconflicts.update(conflicts)
793 793 elif config == 'warn':
794 794 warnconflicts.update(conflicts)
795 795
796 796 checkunknowndirs = _unknowndirschecker()
797 797 for f, (m, args, msg) in actions.iteritems():
798 798 if m in (ACTION_CREATED, ACTION_DELETED_CHANGED):
799 799 if _checkunknownfile(repo, wctx, mctx, f):
800 800 fileconflicts.add(f)
801 801 elif pathconfig and f not in wctx:
802 802 path = checkunknowndirs(repo, wctx, f)
803 803 if path is not None:
804 804 pathconflicts.add(path)
805 805 elif m == ACTION_LOCAL_DIR_RENAME_GET:
806 806 if _checkunknownfile(repo, wctx, mctx, f, args[0]):
807 807 fileconflicts.add(f)
808 808
809 809 allconflicts = fileconflicts | pathconflicts
810 810 ignoredconflicts = set([c for c in allconflicts
811 811 if repo.dirstate._ignore(c)])
812 812 unknownconflicts = allconflicts - ignoredconflicts
813 813 collectconflicts(ignoredconflicts, ignoredconfig)
814 814 collectconflicts(unknownconflicts, unknownconfig)
815 815 else:
816 816 for f, (m, args, msg) in actions.iteritems():
817 817 if m == ACTION_CREATED_MERGE:
818 818 fl2, anc = args
819 819 different = _checkunknownfile(repo, wctx, mctx, f)
820 820 if repo.dirstate._ignore(f):
821 821 config = ignoredconfig
822 822 else:
823 823 config = unknownconfig
824 824
825 825 # The behavior when force is True is described by this table:
826 826 # config different mergeforce | action backup
827 827 # * n * | get n
828 828 # * y y | merge -
829 829 # abort y n | merge - (1)
830 830 # warn y n | warn + get y
831 831 # ignore y n | get y
832 832 #
833 833 # (1) this is probably the wrong behavior here -- we should
834 834 # probably abort, but some actions like rebases currently
835 835 # don't like an abort happening in the middle of
836 836 # merge.update.
837 837 if not different:
838 838 actions[f] = (ACTION_GET, (fl2, False), 'remote created')
839 839 elif mergeforce or config == 'abort':
840 840 actions[f] = (ACTION_MERGE, (f, f, None, False, anc),
841 841 'remote differs from untracked local')
842 842 elif config == 'abort':
843 843 abortconflicts.add(f)
844 844 else:
845 845 if config == 'warn':
846 846 warnconflicts.add(f)
847 847 actions[f] = (ACTION_GET, (fl2, True), 'remote created')
848 848
849 849 for f in sorted(abortconflicts):
850 850 warn = repo.ui.warn
851 851 if f in pathconflicts:
852 852 if repo.wvfs.isfileorlink(f):
853 853 warn(_("%s: untracked file conflicts with directory\n") % f)
854 854 else:
855 855 warn(_("%s: untracked directory conflicts with file\n") % f)
856 856 else:
857 857 warn(_("%s: untracked file differs\n") % f)
858 858 if abortconflicts:
859 859 raise error.Abort(_("untracked files in working directory "
860 860 "differ from files in requested revision"))
861 861
862 862 for f in sorted(warnconflicts):
863 863 if repo.wvfs.isfileorlink(f):
864 864 repo.ui.warn(_("%s: replacing untracked file\n") % f)
865 865 else:
866 866 repo.ui.warn(_("%s: replacing untracked files in directory\n") % f)
867 867
868 868 for f, (m, args, msg) in actions.iteritems():
869 869 if m == ACTION_CREATED:
870 870 backup = (f in fileconflicts or f in pathconflicts or
871 871 any(p in pathconflicts for p in util.finddirs(f)))
872 872 flags, = args
873 873 actions[f] = (ACTION_GET, (flags, backup), msg)
874 874
875 875 def _forgetremoved(wctx, mctx, branchmerge):
876 876 """
877 877 Forget removed files
878 878
879 879 If we're jumping between revisions (as opposed to merging), and if
880 880 neither the working directory nor the target rev has the file,
881 881 then we need to remove it from the dirstate, to prevent the
882 882 dirstate from listing the file when it is no longer in the
883 883 manifest.
884 884
885 885 If we're merging, and the other revision has removed a file
886 886 that is not present in the working directory, we need to mark it
887 887 as removed.
888 888 """
889 889
890 890 actions = {}
891 891 m = ACTION_FORGET
892 892 if branchmerge:
893 893 m = ACTION_REMOVE
894 894 for f in wctx.deleted():
895 895 if f not in mctx:
896 896 actions[f] = m, None, "forget deleted"
897 897
898 898 if not branchmerge:
899 899 for f in wctx.removed():
900 900 if f not in mctx:
901 901 actions[f] = ACTION_FORGET, None, "forget removed"
902 902
903 903 return actions
904 904
905 905 def _checkcollision(repo, wmf, actions):
906 906 """
907 907 Check for case-folding collisions.
908 908 """
909 909
910 910 # If the repo is narrowed, filter out files outside the narrowspec.
911 911 narrowmatch = repo.narrowmatch()
912 912 if not narrowmatch.always():
913 913 wmf = wmf.matches(narrowmatch)
914 914 if actions:
915 915 narrowactions = {}
916 916 for m, actionsfortype in actions.iteritems():
917 917 narrowactions[m] = []
918 918 for (f, args, msg) in actionsfortype:
919 919 if narrowmatch(f):
920 920 narrowactions[m].append((f, args, msg))
921 921 actions = narrowactions
922 922
923 923 # build provisional merged manifest up
924 924 pmmf = set(wmf)
925 925
926 926 if actions:
927 927 # KEEP and EXEC are no-op
928 928 for m in (ACTION_ADD, ACTION_ADD_MODIFIED, ACTION_FORGET, ACTION_GET,
929 929 ACTION_CHANGED_DELETED, ACTION_DELETED_CHANGED):
930 930 for f, args, msg in actions[m]:
931 931 pmmf.add(f)
932 932 for f, args, msg in actions[ACTION_REMOVE]:
933 933 pmmf.discard(f)
934 934 for f, args, msg in actions[ACTION_DIR_RENAME_MOVE_LOCAL]:
935 935 f2, flags = args
936 936 pmmf.discard(f2)
937 937 pmmf.add(f)
938 938 for f, args, msg in actions[ACTION_LOCAL_DIR_RENAME_GET]:
939 939 pmmf.add(f)
940 940 for f, args, msg in actions[ACTION_MERGE]:
941 941 f1, f2, fa, move, anc = args
942 942 if move:
943 943 pmmf.discard(f1)
944 944 pmmf.add(f)
945 945
946 946 # check case-folding collision in provisional merged manifest
947 947 foldmap = {}
948 948 for f in pmmf:
949 949 fold = util.normcase(f)
950 950 if fold in foldmap:
951 951 raise error.Abort(_("case-folding collision between %s and %s")
952 952 % (f, foldmap[fold]))
953 953 foldmap[fold] = f
954 954
955 955 # check case-folding of directories
956 956 foldprefix = unfoldprefix = lastfull = ''
957 957 for fold, f in sorted(foldmap.items()):
958 958 if fold.startswith(foldprefix) and not f.startswith(unfoldprefix):
959 959 # the folded prefix matches but actual casing is different
960 960 raise error.Abort(_("case-folding collision between "
961 961 "%s and directory of %s") % (lastfull, f))
962 962 foldprefix = fold + '/'
963 963 unfoldprefix = f + '/'
964 964 lastfull = f
965 965
966 966 def driverpreprocess(repo, ms, wctx, labels=None):
967 967 """run the preprocess step of the merge driver, if any
968 968
969 969 This is currently not implemented -- it's an extension point."""
970 970 return True
971 971
972 972 def driverconclude(repo, ms, wctx, labels=None):
973 973 """run the conclude step of the merge driver, if any
974 974
975 975 This is currently not implemented -- it's an extension point."""
976 976 return True
977 977
978 978 def _filesindirs(repo, manifest, dirs):
979 979 """
980 980 Generator that yields pairs of all the files in the manifest that are found
981 981 inside the directories listed in dirs, and which directory they are found
982 982 in.
983 983 """
984 984 for f in manifest:
985 985 for p in util.finddirs(f):
986 986 if p in dirs:
987 987 yield f, p
988 988 break
989 989
990 990 def checkpathconflicts(repo, wctx, mctx, actions):
991 991 """
992 992 Check if any actions introduce path conflicts in the repository, updating
993 993 actions to record or handle the path conflict accordingly.
994 994 """
995 995 mf = wctx.manifest()
996 996
997 997 # The set of local files that conflict with a remote directory.
998 998 localconflicts = set()
999 999
1000 1000 # The set of directories that conflict with a remote file, and so may cause
1001 1001 # conflicts if they still contain any files after the merge.
1002 1002 remoteconflicts = set()
1003 1003
1004 1004 # The set of directories that appear as both a file and a directory in the
1005 1005 # remote manifest. These indicate an invalid remote manifest, which
1006 1006 # can't be updated to cleanly.
1007 1007 invalidconflicts = set()
1008 1008
1009 1009 # The set of directories that contain files that are being created.
1010 1010 createdfiledirs = set()
1011 1011
1012 1012 # The set of files deleted by all the actions.
1013 1013 deletedfiles = set()
1014 1014
1015 1015 for f, (m, args, msg) in actions.items():
1016 1016 if m in (ACTION_CREATED, ACTION_DELETED_CHANGED, ACTION_MERGE,
1017 1017 ACTION_CREATED_MERGE):
1018 1018 # This action may create a new local file.
1019 1019 createdfiledirs.update(util.finddirs(f))
1020 1020 if mf.hasdir(f):
1021 1021 # The file aliases a local directory. This might be ok if all
1022 1022 # the files in the local directory are being deleted. This
1023 1023 # will be checked once we know what all the deleted files are.
1024 1024 remoteconflicts.add(f)
1025 1025 # Track the names of all deleted files.
1026 1026 if m == ACTION_REMOVE:
1027 1027 deletedfiles.add(f)
1028 1028 if m == ACTION_MERGE:
1029 1029 f1, f2, fa, move, anc = args
1030 1030 if move:
1031 1031 deletedfiles.add(f1)
1032 1032 if m == ACTION_DIR_RENAME_MOVE_LOCAL:
1033 1033 f2, flags = args
1034 1034 deletedfiles.add(f2)
1035 1035
1036 1036 # Check all directories that contain created files for path conflicts.
1037 1037 for p in createdfiledirs:
1038 1038 if p in mf:
1039 1039 if p in mctx:
1040 1040 # A file is in a directory which aliases both a local
1041 1041 # and a remote file. This is an internal inconsistency
1042 1042 # within the remote manifest.
1043 1043 invalidconflicts.add(p)
1044 1044 else:
1045 1045 # A file is in a directory which aliases a local file.
1046 1046 # We will need to rename the local file.
1047 1047 localconflicts.add(p)
1048 1048 if p in actions and actions[p][0] in (ACTION_CREATED,
1049 1049 ACTION_DELETED_CHANGED,
1050 1050 ACTION_MERGE,
1051 1051 ACTION_CREATED_MERGE):
1052 1052 # The file is in a directory which aliases a remote file.
1053 1053 # This is an internal inconsistency within the remote
1054 1054 # manifest.
1055 1055 invalidconflicts.add(p)
1056 1056
1057 1057 # Rename all local conflicting files that have not been deleted.
1058 1058 for p in localconflicts:
1059 1059 if p not in deletedfiles:
1060 1060 ctxname = bytes(wctx).rstrip('+')
1061 1061 pnew = util.safename(p, ctxname, wctx, set(actions.keys()))
1062 1062 actions[pnew] = (ACTION_PATH_CONFLICT_RESOLVE, (p,),
1063 1063 'local path conflict')
1064 1064 actions[p] = (ACTION_PATH_CONFLICT, (pnew, 'l'),
1065 1065 'path conflict')
1066 1066
1067 1067 if remoteconflicts:
1068 1068 # Check if all files in the conflicting directories have been removed.
1069 1069 ctxname = bytes(mctx).rstrip('+')
1070 1070 for f, p in _filesindirs(repo, mf, remoteconflicts):
1071 1071 if f not in deletedfiles:
1072 1072 m, args, msg = actions[p]
1073 1073 pnew = util.safename(p, ctxname, wctx, set(actions.keys()))
1074 1074 if m in (ACTION_DELETED_CHANGED, ACTION_MERGE):
1075 1075 # Action was merge, just update target.
1076 1076 actions[pnew] = (m, args, msg)
1077 1077 else:
1078 1078 # Action was create, change to renamed get action.
1079 1079 fl = args[0]
1080 1080 actions[pnew] = (ACTION_LOCAL_DIR_RENAME_GET, (p, fl),
1081 1081 'remote path conflict')
1082 1082 actions[p] = (ACTION_PATH_CONFLICT, (pnew, ACTION_REMOVE),
1083 1083 'path conflict')
1084 1084 remoteconflicts.remove(p)
1085 1085 break
1086 1086
1087 1087 if invalidconflicts:
1088 1088 for p in invalidconflicts:
1089 1089 repo.ui.warn(_("%s: is both a file and a directory\n") % p)
1090 1090 raise error.Abort(_("destination manifest contains path conflicts"))
1091 1091
1092 1092 def _filternarrowactions(narrowmatch, branchmerge, actions):
1093 1093 """
1094 1094 Filters out actions that can ignored because the repo is narrowed.
1095 1095
1096 1096 Raise an exception if the merge cannot be completed because the repo is
1097 1097 narrowed.
1098 1098 """
1099 1099 nooptypes = set(['k']) # TODO: handle with nonconflicttypes
1100 1100 nonconflicttypes = set('a am c cm f g r e'.split())
1101 1101 # We mutate the items in the dict during iteration, so iterate
1102 1102 # over a copy.
1103 1103 for f, action in list(actions.items()):
1104 1104 if narrowmatch(f):
1105 1105 pass
1106 1106 elif not branchmerge:
1107 1107 del actions[f] # just updating, ignore changes outside clone
1108 1108 elif action[0] in nooptypes:
1109 1109 del actions[f] # merge does not affect file
1110 1110 elif action[0] in nonconflicttypes:
1111 1111 raise error.Abort(_('merge affects file \'%s\' outside narrow, '
1112 1112 'which is not yet supported') % f,
1113 1113 hint=_('merging in the other direction '
1114 1114 'may work'))
1115 1115 else:
1116 1116 raise error.Abort(_('conflict in file \'%s\' is outside '
1117 1117 'narrow clone') % f)
1118 1118
1119 1119 def manifestmerge(repo, wctx, p2, pa, branchmerge, force, matcher,
1120 1120 acceptremote, followcopies, forcefulldiff=False):
1121 1121 """
1122 1122 Merge wctx and p2 with ancestor pa and generate merge action list
1123 1123
1124 1124 branchmerge and force are as passed in to update
1125 1125 matcher = matcher to filter file lists
1126 1126 acceptremote = accept the incoming changes without prompting
1127 1127 """
1128 1128 if matcher is not None and matcher.always():
1129 1129 matcher = None
1130 1130
1131 1131 copy, movewithdir, diverge, renamedelete, dirmove = {}, {}, {}, {}, {}
1132 1132
1133 1133 # manifests fetched in order are going to be faster, so prime the caches
1134 1134 [x.manifest() for x in
1135 1135 sorted(wctx.parents() + [p2, pa], key=scmutil.intrev)]
1136 1136
1137 1137 if followcopies:
1138 1138 ret = copies.mergecopies(repo, wctx, p2, pa)
1139 1139 copy, movewithdir, diverge, renamedelete, dirmove = ret
1140 1140
1141 1141 boolbm = pycompat.bytestr(bool(branchmerge))
1142 1142 boolf = pycompat.bytestr(bool(force))
1143 1143 boolm = pycompat.bytestr(bool(matcher))
1144 1144 repo.ui.note(_("resolving manifests\n"))
1145 1145 repo.ui.debug(" branchmerge: %s, force: %s, partial: %s\n"
1146 1146 % (boolbm, boolf, boolm))
1147 1147 repo.ui.debug(" ancestor: %s, local: %s, remote: %s\n" % (pa, wctx, p2))
1148 1148
1149 1149 m1, m2, ma = wctx.manifest(), p2.manifest(), pa.manifest()
1150 1150 copied = set(copy.values())
1151 1151 copied.update(movewithdir.values())
1152 1152
1153 if '.hgsubstate' in m1:
1154 # check whether sub state is modified
1153 if '.hgsubstate' in m1 and wctx.rev() is None:
1154 # Check whether sub state is modified, and overwrite the manifest
1155 # to flag the change. If wctx is a committed revision, we shouldn't
1156 # care for the dirty state of the working directory.
1155 1157 if any(wctx.sub(s).dirty() for s in wctx.substate):
1156 1158 m1['.hgsubstate'] = modifiednodeid
1157 1159
1158 1160 # Don't use m2-vs-ma optimization if:
1159 1161 # - ma is the same as m1 or m2, which we're just going to diff again later
1160 1162 # - The caller specifically asks for a full diff, which is useful during bid
1161 1163 # merge.
1162 1164 if (pa not in ([wctx, p2] + wctx.parents()) and not forcefulldiff):
1163 1165 # Identify which files are relevant to the merge, so we can limit the
1164 1166 # total m1-vs-m2 diff to just those files. This has significant
1165 1167 # performance benefits in large repositories.
1166 1168 relevantfiles = set(ma.diff(m2).keys())
1167 1169
1168 1170 # For copied and moved files, we need to add the source file too.
1169 1171 for copykey, copyvalue in copy.iteritems():
1170 1172 if copyvalue in relevantfiles:
1171 1173 relevantfiles.add(copykey)
1172 1174 for movedirkey in movewithdir:
1173 1175 relevantfiles.add(movedirkey)
1174 1176 filesmatcher = scmutil.matchfiles(repo, relevantfiles)
1175 1177 matcher = matchmod.intersectmatchers(matcher, filesmatcher)
1176 1178
1177 1179 diff = m1.diff(m2, match=matcher)
1178 1180
1179 1181 if matcher is None:
1180 1182 matcher = matchmod.always('', '')
1181 1183
1182 1184 actions = {}
1183 1185 for f, ((n1, fl1), (n2, fl2)) in diff.iteritems():
1184 1186 if n1 and n2: # file exists on both local and remote side
1185 1187 if f not in ma:
1186 1188 fa = copy.get(f, None)
1187 1189 if fa is not None:
1188 1190 actions[f] = (ACTION_MERGE, (f, f, fa, False, pa.node()),
1189 1191 'both renamed from %s' % fa)
1190 1192 else:
1191 1193 actions[f] = (ACTION_MERGE, (f, f, None, False, pa.node()),
1192 1194 'both created')
1193 1195 else:
1194 1196 a = ma[f]
1195 1197 fla = ma.flags(f)
1196 1198 nol = 'l' not in fl1 + fl2 + fla
1197 1199 if n2 == a and fl2 == fla:
1198 1200 actions[f] = (ACTION_KEEP, (), 'remote unchanged')
1199 1201 elif n1 == a and fl1 == fla: # local unchanged - use remote
1200 1202 if n1 == n2: # optimization: keep local content
1201 1203 actions[f] = (ACTION_EXEC, (fl2,), 'update permissions')
1202 1204 else:
1203 1205 actions[f] = (ACTION_GET, (fl2, False),
1204 1206 'remote is newer')
1205 1207 elif nol and n2 == a: # remote only changed 'x'
1206 1208 actions[f] = (ACTION_EXEC, (fl2,), 'update permissions')
1207 1209 elif nol and n1 == a: # local only changed 'x'
1208 1210 actions[f] = (ACTION_GET, (fl1, False), 'remote is newer')
1209 1211 else: # both changed something
1210 1212 actions[f] = (ACTION_MERGE, (f, f, f, False, pa.node()),
1211 1213 'versions differ')
1212 1214 elif n1: # file exists only on local side
1213 1215 if f in copied:
1214 1216 pass # we'll deal with it on m2 side
1215 1217 elif f in movewithdir: # directory rename, move local
1216 1218 f2 = movewithdir[f]
1217 1219 if f2 in m2:
1218 1220 actions[f2] = (ACTION_MERGE, (f, f2, None, True, pa.node()),
1219 1221 'remote directory rename, both created')
1220 1222 else:
1221 1223 actions[f2] = (ACTION_DIR_RENAME_MOVE_LOCAL, (f, fl1),
1222 1224 'remote directory rename - move from %s' % f)
1223 1225 elif f in copy:
1224 1226 f2 = copy[f]
1225 1227 actions[f] = (ACTION_MERGE, (f, f2, f2, False, pa.node()),
1226 1228 'local copied/moved from %s' % f2)
1227 1229 elif f in ma: # clean, a different, no remote
1228 1230 if n1 != ma[f]:
1229 1231 if acceptremote:
1230 1232 actions[f] = (ACTION_REMOVE, None, 'remote delete')
1231 1233 else:
1232 1234 actions[f] = (ACTION_CHANGED_DELETED,
1233 1235 (f, None, f, False, pa.node()),
1234 1236 'prompt changed/deleted')
1235 1237 elif n1 == addednodeid:
1236 1238 # This extra 'a' is added by working copy manifest to mark
1237 1239 # the file as locally added. We should forget it instead of
1238 1240 # deleting it.
1239 1241 actions[f] = (ACTION_FORGET, None, 'remote deleted')
1240 1242 else:
1241 1243 actions[f] = (ACTION_REMOVE, None, 'other deleted')
1242 1244 elif n2: # file exists only on remote side
1243 1245 if f in copied:
1244 1246 pass # we'll deal with it on m1 side
1245 1247 elif f in movewithdir:
1246 1248 f2 = movewithdir[f]
1247 1249 if f2 in m1:
1248 1250 actions[f2] = (ACTION_MERGE,
1249 1251 (f2, f, None, False, pa.node()),
1250 1252 'local directory rename, both created')
1251 1253 else:
1252 1254 actions[f2] = (ACTION_LOCAL_DIR_RENAME_GET, (f, fl2),
1253 1255 'local directory rename - get from %s' % f)
1254 1256 elif f in copy:
1255 1257 f2 = copy[f]
1256 1258 if f2 in m2:
1257 1259 actions[f] = (ACTION_MERGE, (f2, f, f2, False, pa.node()),
1258 1260 'remote copied from %s' % f2)
1259 1261 else:
1260 1262 actions[f] = (ACTION_MERGE, (f2, f, f2, True, pa.node()),
1261 1263 'remote moved from %s' % f2)
1262 1264 elif f not in ma:
1263 1265 # local unknown, remote created: the logic is described by the
1264 1266 # following table:
1265 1267 #
1266 1268 # force branchmerge different | action
1267 1269 # n * * | create
1268 1270 # y n * | create
1269 1271 # y y n | create
1270 1272 # y y y | merge
1271 1273 #
1272 1274 # Checking whether the files are different is expensive, so we
1273 1275 # don't do that when we can avoid it.
1274 1276 if not force:
1275 1277 actions[f] = (ACTION_CREATED, (fl2,), 'remote created')
1276 1278 elif not branchmerge:
1277 1279 actions[f] = (ACTION_CREATED, (fl2,), 'remote created')
1278 1280 else:
1279 1281 actions[f] = (ACTION_CREATED_MERGE, (fl2, pa.node()),
1280 1282 'remote created, get or merge')
1281 1283 elif n2 != ma[f]:
1282 1284 df = None
1283 1285 for d in dirmove:
1284 1286 if f.startswith(d):
1285 1287 # new file added in a directory that was moved
1286 1288 df = dirmove[d] + f[len(d):]
1287 1289 break
1288 1290 if df is not None and df in m1:
1289 1291 actions[df] = (ACTION_MERGE, (df, f, f, False, pa.node()),
1290 1292 'local directory rename - respect move '
1291 1293 'from %s' % f)
1292 1294 elif acceptremote:
1293 1295 actions[f] = (ACTION_CREATED, (fl2,), 'remote recreating')
1294 1296 else:
1295 1297 actions[f] = (ACTION_DELETED_CHANGED,
1296 1298 (None, f, f, False, pa.node()),
1297 1299 'prompt deleted/changed')
1298 1300
1299 1301 if repo.ui.configbool('experimental', 'merge.checkpathconflicts'):
1300 1302 # If we are merging, look for path conflicts.
1301 1303 checkpathconflicts(repo, wctx, p2, actions)
1302 1304
1303 1305 narrowmatch = repo.narrowmatch()
1304 1306 if not narrowmatch.always():
1305 1307 # Updates "actions" in place
1306 1308 _filternarrowactions(narrowmatch, branchmerge, actions)
1307 1309
1308 1310 return actions, diverge, renamedelete
1309 1311
1310 1312 def _resolvetrivial(repo, wctx, mctx, ancestor, actions):
1311 1313 """Resolves false conflicts where the nodeid changed but the content
1312 1314 remained the same."""
1313 1315 # We force a copy of actions.items() because we're going to mutate
1314 1316 # actions as we resolve trivial conflicts.
1315 1317 for f, (m, args, msg) in list(actions.items()):
1316 1318 if (m == ACTION_CHANGED_DELETED and f in ancestor
1317 1319 and not wctx[f].cmp(ancestor[f])):
1318 1320 # local did change but ended up with same content
1319 1321 actions[f] = ACTION_REMOVE, None, 'prompt same'
1320 1322 elif (m == ACTION_DELETED_CHANGED and f in ancestor
1321 1323 and not mctx[f].cmp(ancestor[f])):
1322 1324 # remote did change but ended up with same content
1323 1325 del actions[f] # don't get = keep local deleted
1324 1326
1325 1327 def calculateupdates(repo, wctx, mctx, ancestors, branchmerge, force,
1326 1328 acceptremote, followcopies, matcher=None,
1327 1329 mergeforce=False):
1328 1330 """Calculate the actions needed to merge mctx into wctx using ancestors"""
1329 1331 # Avoid cycle.
1330 1332 from . import sparse
1331 1333
1332 1334 if len(ancestors) == 1: # default
1333 1335 actions, diverge, renamedelete = manifestmerge(
1334 1336 repo, wctx, mctx, ancestors[0], branchmerge, force, matcher,
1335 1337 acceptremote, followcopies)
1336 1338 _checkunknownfiles(repo, wctx, mctx, force, actions, mergeforce)
1337 1339
1338 1340 else: # only when merge.preferancestor=* - the default
1339 1341 repo.ui.note(
1340 1342 _("note: merging %s and %s using bids from ancestors %s\n") %
1341 1343 (wctx, mctx, _(' and ').join(pycompat.bytestr(anc)
1342 1344 for anc in ancestors)))
1343 1345
1344 1346 # Call for bids
1345 1347 fbids = {} # mapping filename to bids (action method to list af actions)
1346 1348 diverge, renamedelete = None, None
1347 1349 for ancestor in ancestors:
1348 1350 repo.ui.note(_('\ncalculating bids for ancestor %s\n') % ancestor)
1349 1351 actions, diverge1, renamedelete1 = manifestmerge(
1350 1352 repo, wctx, mctx, ancestor, branchmerge, force, matcher,
1351 1353 acceptremote, followcopies, forcefulldiff=True)
1352 1354 _checkunknownfiles(repo, wctx, mctx, force, actions, mergeforce)
1353 1355
1354 1356 # Track the shortest set of warning on the theory that bid
1355 1357 # merge will correctly incorporate more information
1356 1358 if diverge is None or len(diverge1) < len(diverge):
1357 1359 diverge = diverge1
1358 1360 if renamedelete is None or len(renamedelete) < len(renamedelete1):
1359 1361 renamedelete = renamedelete1
1360 1362
1361 1363 for f, a in sorted(actions.iteritems()):
1362 1364 m, args, msg = a
1363 1365 repo.ui.debug(' %s: %s -> %s\n' % (f, msg, m))
1364 1366 if f in fbids:
1365 1367 d = fbids[f]
1366 1368 if m in d:
1367 1369 d[m].append(a)
1368 1370 else:
1369 1371 d[m] = [a]
1370 1372 else:
1371 1373 fbids[f] = {m: [a]}
1372 1374
1373 1375 # Pick the best bid for each file
1374 1376 repo.ui.note(_('\nauction for merging merge bids\n'))
1375 1377 actions = {}
1376 1378 dms = [] # filenames that have dm actions
1377 1379 for f, bids in sorted(fbids.items()):
1378 1380 # bids is a mapping from action method to list af actions
1379 1381 # Consensus?
1380 1382 if len(bids) == 1: # all bids are the same kind of method
1381 1383 m, l = list(bids.items())[0]
1382 1384 if all(a == l[0] for a in l[1:]): # len(bids) is > 1
1383 1385 repo.ui.note(_(" %s: consensus for %s\n") % (f, m))
1384 1386 actions[f] = l[0]
1385 1387 if m == ACTION_DIR_RENAME_MOVE_LOCAL:
1386 1388 dms.append(f)
1387 1389 continue
1388 1390 # If keep is an option, just do it.
1389 1391 if ACTION_KEEP in bids:
1390 1392 repo.ui.note(_(" %s: picking 'keep' action\n") % f)
1391 1393 actions[f] = bids[ACTION_KEEP][0]
1392 1394 continue
1393 1395 # If there are gets and they all agree [how could they not?], do it.
1394 1396 if ACTION_GET in bids:
1395 1397 ga0 = bids[ACTION_GET][0]
1396 1398 if all(a == ga0 for a in bids[ACTION_GET][1:]):
1397 1399 repo.ui.note(_(" %s: picking 'get' action\n") % f)
1398 1400 actions[f] = ga0
1399 1401 continue
1400 1402 # TODO: Consider other simple actions such as mode changes
1401 1403 # Handle inefficient democrazy.
1402 1404 repo.ui.note(_(' %s: multiple bids for merge action:\n') % f)
1403 1405 for m, l in sorted(bids.items()):
1404 1406 for _f, args, msg in l:
1405 1407 repo.ui.note(' %s -> %s\n' % (msg, m))
1406 1408 # Pick random action. TODO: Instead, prompt user when resolving
1407 1409 m, l = list(bids.items())[0]
1408 1410 repo.ui.warn(_(' %s: ambiguous merge - picked %s action\n') %
1409 1411 (f, m))
1410 1412 actions[f] = l[0]
1411 1413 if m == ACTION_DIR_RENAME_MOVE_LOCAL:
1412 1414 dms.append(f)
1413 1415 continue
1414 1416 # Work around 'dm' that can cause multiple actions for the same file
1415 1417 for f in dms:
1416 1418 dm, (f0, flags), msg = actions[f]
1417 1419 assert dm == ACTION_DIR_RENAME_MOVE_LOCAL, dm
1418 1420 if f0 in actions and actions[f0][0] == ACTION_REMOVE:
1419 1421 # We have one bid for removing a file and another for moving it.
1420 1422 # These two could be merged as first move and then delete ...
1421 1423 # but instead drop moving and just delete.
1422 1424 del actions[f]
1423 1425 repo.ui.note(_('end of auction\n\n'))
1424 1426
1425 1427 _resolvetrivial(repo, wctx, mctx, ancestors[0], actions)
1426 1428
1427 1429 if wctx.rev() is None:
1428 1430 fractions = _forgetremoved(wctx, mctx, branchmerge)
1429 1431 actions.update(fractions)
1430 1432
1431 1433 prunedactions = sparse.filterupdatesactions(repo, wctx, mctx, branchmerge,
1432 1434 actions)
1433 1435
1434 1436 return prunedactions, diverge, renamedelete
1435 1437
1436 1438 def _getcwd():
1437 1439 try:
1438 1440 return pycompat.getcwd()
1439 1441 except OSError as err:
1440 1442 if err.errno == errno.ENOENT:
1441 1443 return None
1442 1444 raise
1443 1445
1444 1446 def batchremove(repo, wctx, actions):
1445 1447 """apply removes to the working directory
1446 1448
1447 1449 yields tuples for progress updates
1448 1450 """
1449 1451 verbose = repo.ui.verbose
1450 1452 cwd = _getcwd()
1451 1453 i = 0
1452 1454 for f, args, msg in actions:
1453 1455 repo.ui.debug(" %s: %s -> r\n" % (f, msg))
1454 1456 if verbose:
1455 1457 repo.ui.note(_("removing %s\n") % f)
1456 1458 wctx[f].audit()
1457 1459 try:
1458 1460 wctx[f].remove(ignoremissing=True)
1459 1461 except OSError as inst:
1460 1462 repo.ui.warn(_("update failed to remove %s: %s!\n") %
1461 1463 (f, inst.strerror))
1462 1464 if i == 100:
1463 1465 yield i, f
1464 1466 i = 0
1465 1467 i += 1
1466 1468 if i > 0:
1467 1469 yield i, f
1468 1470
1469 1471 if cwd and not _getcwd():
1470 1472 # cwd was removed in the course of removing files; print a helpful
1471 1473 # warning.
1472 1474 repo.ui.warn(_("current directory was removed\n"
1473 1475 "(consider changing to repo root: %s)\n") % repo.root)
1474 1476
1475 1477 def batchget(repo, mctx, wctx, actions):
1476 1478 """apply gets to the working directory
1477 1479
1478 1480 mctx is the context to get from
1479 1481
1480 1482 yields tuples for progress updates
1481 1483 """
1482 1484 verbose = repo.ui.verbose
1483 1485 fctx = mctx.filectx
1484 1486 ui = repo.ui
1485 1487 i = 0
1486 1488 with repo.wvfs.backgroundclosing(ui, expectedcount=len(actions)):
1487 1489 for f, (flags, backup), msg in actions:
1488 1490 repo.ui.debug(" %s: %s -> g\n" % (f, msg))
1489 1491 if verbose:
1490 1492 repo.ui.note(_("getting %s\n") % f)
1491 1493
1492 1494 if backup:
1493 1495 # If a file or directory exists with the same name, back that
1494 1496 # up. Otherwise, look to see if there is a file that conflicts
1495 1497 # with a directory this file is in, and if so, back that up.
1496 1498 absf = repo.wjoin(f)
1497 1499 if not repo.wvfs.lexists(f):
1498 1500 for p in util.finddirs(f):
1499 1501 if repo.wvfs.isfileorlink(p):
1500 1502 absf = repo.wjoin(p)
1501 1503 break
1502 1504 orig = scmutil.origpath(ui, repo, absf)
1503 1505 if repo.wvfs.lexists(absf):
1504 1506 util.rename(absf, orig)
1505 1507 wctx[f].clearunknown()
1506 1508 atomictemp = ui.configbool("experimental", "update.atomic-file")
1507 1509 wctx[f].write(fctx(f).data(), flags, backgroundclose=True,
1508 1510 atomictemp=atomictemp)
1509 1511 if i == 100:
1510 1512 yield i, f
1511 1513 i = 0
1512 1514 i += 1
1513 1515 if i > 0:
1514 1516 yield i, f
1515 1517
1516 1518 def _prefetchfiles(repo, ctx, actions):
1517 1519 """Invoke ``scmutil.prefetchfiles()`` for the files relevant to the dict
1518 1520 of merge actions. ``ctx`` is the context being merged in."""
1519 1521
1520 1522 # Skipping 'a', 'am', 'f', 'r', 'dm', 'e', 'k', 'p' and 'pr', because they
1521 1523 # don't touch the context to be merged in. 'cd' is skipped, because
1522 1524 # changed/deleted never resolves to something from the remote side.
1523 1525 oplist = [actions[a] for a in (ACTION_GET, ACTION_DELETED_CHANGED,
1524 1526 ACTION_LOCAL_DIR_RENAME_GET, ACTION_MERGE)]
1525 1527 prefetch = scmutil.prefetchfiles
1526 1528 matchfiles = scmutil.matchfiles
1527 1529 prefetch(repo, [ctx.rev()],
1528 1530 matchfiles(repo,
1529 1531 [f for sublist in oplist for f, args, msg in sublist]))
1530 1532
1531 1533 @attr.s(frozen=True)
1532 1534 class updateresult(object):
1533 1535 updatedcount = attr.ib()
1534 1536 mergedcount = attr.ib()
1535 1537 removedcount = attr.ib()
1536 1538 unresolvedcount = attr.ib()
1537 1539
1538 1540 def isempty(self):
1539 1541 return (not self.updatedcount and not self.mergedcount
1540 1542 and not self.removedcount and not self.unresolvedcount)
1541 1543
1542 1544 def applyupdates(repo, actions, wctx, mctx, overwrite, labels=None):
1543 1545 """apply the merge action list to the working directory
1544 1546
1545 1547 wctx is the working copy context
1546 1548 mctx is the context to be merged into the working copy
1547 1549
1548 1550 Return a tuple of counts (updated, merged, removed, unresolved) that
1549 1551 describes how many files were affected by the update.
1550 1552 """
1551 1553
1552 1554 _prefetchfiles(repo, mctx, actions)
1553 1555
1554 1556 updated, merged, removed = 0, 0, 0
1555 1557 ms = mergestate.clean(repo, wctx.p1().node(), mctx.node(), labels)
1556 1558 moves = []
1557 1559 for m, l in actions.items():
1558 1560 l.sort()
1559 1561
1560 1562 # 'cd' and 'dc' actions are treated like other merge conflicts
1561 1563 mergeactions = sorted(actions[ACTION_CHANGED_DELETED])
1562 1564 mergeactions.extend(sorted(actions[ACTION_DELETED_CHANGED]))
1563 1565 mergeactions.extend(actions[ACTION_MERGE])
1564 1566 for f, args, msg in mergeactions:
1565 1567 f1, f2, fa, move, anc = args
1566 1568 if f == '.hgsubstate': # merged internally
1567 1569 continue
1568 1570 if f1 is None:
1569 1571 fcl = filemerge.absentfilectx(wctx, fa)
1570 1572 else:
1571 1573 repo.ui.debug(" preserving %s for resolve of %s\n" % (f1, f))
1572 1574 fcl = wctx[f1]
1573 1575 if f2 is None:
1574 1576 fco = filemerge.absentfilectx(mctx, fa)
1575 1577 else:
1576 1578 fco = mctx[f2]
1577 1579 actx = repo[anc]
1578 1580 if fa in actx:
1579 1581 fca = actx[fa]
1580 1582 else:
1581 1583 # TODO: move to absentfilectx
1582 1584 fca = repo.filectx(f1, fileid=nullrev)
1583 1585 ms.add(fcl, fco, fca, f)
1584 1586 if f1 != f and move:
1585 1587 moves.append(f1)
1586 1588
1587 1589 # remove renamed files after safely stored
1588 1590 for f in moves:
1589 1591 if wctx[f].lexists():
1590 1592 repo.ui.debug("removing %s\n" % f)
1591 1593 wctx[f].audit()
1592 1594 wctx[f].remove()
1593 1595
1594 1596 numupdates = sum(len(l) for m, l in actions.items()
1595 1597 if m != ACTION_KEEP)
1596 1598 progress = repo.ui.makeprogress(_('updating'), unit=_('files'),
1597 1599 total=numupdates)
1598 1600
1599 1601 if [a for a in actions[ACTION_REMOVE] if a[0] == '.hgsubstate']:
1600 1602 subrepoutil.submerge(repo, wctx, mctx, wctx, overwrite, labels)
1601 1603
1602 1604 # record path conflicts
1603 1605 for f, args, msg in actions[ACTION_PATH_CONFLICT]:
1604 1606 f1, fo = args
1605 1607 s = repo.ui.status
1606 1608 s(_("%s: path conflict - a file or link has the same name as a "
1607 1609 "directory\n") % f)
1608 1610 if fo == 'l':
1609 1611 s(_("the local file has been renamed to %s\n") % f1)
1610 1612 else:
1611 1613 s(_("the remote file has been renamed to %s\n") % f1)
1612 1614 s(_("resolve manually then use 'hg resolve --mark %s'\n") % f)
1613 1615 ms.addpath(f, f1, fo)
1614 1616 progress.increment(item=f)
1615 1617
1616 1618 # When merging in-memory, we can't support worker processes, so set the
1617 1619 # per-item cost at 0 in that case.
1618 1620 cost = 0 if wctx.isinmemory() else 0.001
1619 1621
1620 1622 # remove in parallel (must come before resolving path conflicts and getting)
1621 1623 prog = worker.worker(repo.ui, cost, batchremove, (repo, wctx),
1622 1624 actions[ACTION_REMOVE])
1623 1625 for i, item in prog:
1624 1626 progress.increment(step=i, item=item)
1625 1627 removed = len(actions[ACTION_REMOVE])
1626 1628
1627 1629 # resolve path conflicts (must come before getting)
1628 1630 for f, args, msg in actions[ACTION_PATH_CONFLICT_RESOLVE]:
1629 1631 repo.ui.debug(" %s: %s -> pr\n" % (f, msg))
1630 1632 f0, = args
1631 1633 if wctx[f0].lexists():
1632 1634 repo.ui.note(_("moving %s to %s\n") % (f0, f))
1633 1635 wctx[f].audit()
1634 1636 wctx[f].write(wctx.filectx(f0).data(), wctx.filectx(f0).flags())
1635 1637 wctx[f0].remove()
1636 1638 progress.increment(item=f)
1637 1639
1638 1640 # get in parallel
1639 1641 prog = worker.worker(repo.ui, cost, batchget, (repo, mctx, wctx),
1640 1642 actions[ACTION_GET])
1641 1643 for i, item in prog:
1642 1644 progress.increment(step=i, item=item)
1643 1645 updated = len(actions[ACTION_GET])
1644 1646
1645 1647 if [a for a in actions[ACTION_GET] if a[0] == '.hgsubstate']:
1646 1648 subrepoutil.submerge(repo, wctx, mctx, wctx, overwrite, labels)
1647 1649
1648 1650 # forget (manifest only, just log it) (must come first)
1649 1651 for f, args, msg in actions[ACTION_FORGET]:
1650 1652 repo.ui.debug(" %s: %s -> f\n" % (f, msg))
1651 1653 progress.increment(item=f)
1652 1654
1653 1655 # re-add (manifest only, just log it)
1654 1656 for f, args, msg in actions[ACTION_ADD]:
1655 1657 repo.ui.debug(" %s: %s -> a\n" % (f, msg))
1656 1658 progress.increment(item=f)
1657 1659
1658 1660 # re-add/mark as modified (manifest only, just log it)
1659 1661 for f, args, msg in actions[ACTION_ADD_MODIFIED]:
1660 1662 repo.ui.debug(" %s: %s -> am\n" % (f, msg))
1661 1663 progress.increment(item=f)
1662 1664
1663 1665 # keep (noop, just log it)
1664 1666 for f, args, msg in actions[ACTION_KEEP]:
1665 1667 repo.ui.debug(" %s: %s -> k\n" % (f, msg))
1666 1668 # no progress
1667 1669
1668 1670 # directory rename, move local
1669 1671 for f, args, msg in actions[ACTION_DIR_RENAME_MOVE_LOCAL]:
1670 1672 repo.ui.debug(" %s: %s -> dm\n" % (f, msg))
1671 1673 progress.increment(item=f)
1672 1674 f0, flags = args
1673 1675 repo.ui.note(_("moving %s to %s\n") % (f0, f))
1674 1676 wctx[f].audit()
1675 1677 wctx[f].write(wctx.filectx(f0).data(), flags)
1676 1678 wctx[f0].remove()
1677 1679 updated += 1
1678 1680
1679 1681 # local directory rename, get
1680 1682 for f, args, msg in actions[ACTION_LOCAL_DIR_RENAME_GET]:
1681 1683 repo.ui.debug(" %s: %s -> dg\n" % (f, msg))
1682 1684 progress.increment(item=f)
1683 1685 f0, flags = args
1684 1686 repo.ui.note(_("getting %s to %s\n") % (f0, f))
1685 1687 wctx[f].write(mctx.filectx(f0).data(), flags)
1686 1688 updated += 1
1687 1689
1688 1690 # exec
1689 1691 for f, args, msg in actions[ACTION_EXEC]:
1690 1692 repo.ui.debug(" %s: %s -> e\n" % (f, msg))
1691 1693 progress.increment(item=f)
1692 1694 flags, = args
1693 1695 wctx[f].audit()
1694 1696 wctx[f].setflags('l' in flags, 'x' in flags)
1695 1697 updated += 1
1696 1698
1697 1699 # the ordering is important here -- ms.mergedriver will raise if the merge
1698 1700 # driver has changed, and we want to be able to bypass it when overwrite is
1699 1701 # True
1700 1702 usemergedriver = not overwrite and mergeactions and ms.mergedriver
1701 1703
1702 1704 if usemergedriver:
1703 1705 if wctx.isinmemory():
1704 1706 raise error.InMemoryMergeConflictsError("in-memory merge does not "
1705 1707 "support mergedriver")
1706 1708 ms.commit()
1707 1709 proceed = driverpreprocess(repo, ms, wctx, labels=labels)
1708 1710 # the driver might leave some files unresolved
1709 1711 unresolvedf = set(ms.unresolved())
1710 1712 if not proceed:
1711 1713 # XXX setting unresolved to at least 1 is a hack to make sure we
1712 1714 # error out
1713 1715 return updateresult(updated, merged, removed,
1714 1716 max(len(unresolvedf), 1))
1715 1717 newactions = []
1716 1718 for f, args, msg in mergeactions:
1717 1719 if f in unresolvedf:
1718 1720 newactions.append((f, args, msg))
1719 1721 mergeactions = newactions
1720 1722
1721 1723 try:
1722 1724 # premerge
1723 1725 tocomplete = []
1724 1726 for f, args, msg in mergeactions:
1725 1727 repo.ui.debug(" %s: %s -> m (premerge)\n" % (f, msg))
1726 1728 progress.increment(item=f)
1727 1729 if f == '.hgsubstate': # subrepo states need updating
1728 1730 subrepoutil.submerge(repo, wctx, mctx, wctx.ancestor(mctx),
1729 1731 overwrite, labels)
1730 1732 continue
1731 1733 wctx[f].audit()
1732 1734 complete, r = ms.preresolve(f, wctx)
1733 1735 if not complete:
1734 1736 numupdates += 1
1735 1737 tocomplete.append((f, args, msg))
1736 1738
1737 1739 # merge
1738 1740 for f, args, msg in tocomplete:
1739 1741 repo.ui.debug(" %s: %s -> m (merge)\n" % (f, msg))
1740 1742 progress.increment(item=f, total=numupdates)
1741 1743 ms.resolve(f, wctx)
1742 1744
1743 1745 finally:
1744 1746 ms.commit()
1745 1747
1746 1748 unresolved = ms.unresolvedcount()
1747 1749
1748 1750 if (usemergedriver and not unresolved
1749 1751 and ms.mdstate() != MERGE_DRIVER_STATE_SUCCESS):
1750 1752 if not driverconclude(repo, ms, wctx, labels=labels):
1751 1753 # XXX setting unresolved to at least 1 is a hack to make sure we
1752 1754 # error out
1753 1755 unresolved = max(unresolved, 1)
1754 1756
1755 1757 ms.commit()
1756 1758
1757 1759 msupdated, msmerged, msremoved = ms.counts()
1758 1760 updated += msupdated
1759 1761 merged += msmerged
1760 1762 removed += msremoved
1761 1763
1762 1764 extraactions = ms.actions()
1763 1765 if extraactions:
1764 1766 mfiles = set(a[0] for a in actions[ACTION_MERGE])
1765 1767 for k, acts in extraactions.iteritems():
1766 1768 actions[k].extend(acts)
1767 1769 # Remove these files from actions[ACTION_MERGE] as well. This is
1768 1770 # important because in recordupdates, files in actions[ACTION_MERGE]
1769 1771 # are processed after files in other actions, and the merge driver
1770 1772 # might add files to those actions via extraactions above. This can
1771 1773 # lead to a file being recorded twice, with poor results. This is
1772 1774 # especially problematic for actions[ACTION_REMOVE] (currently only
1773 1775 # possible with the merge driver in the initial merge process;
1774 1776 # interrupted merges don't go through this flow).
1775 1777 #
1776 1778 # The real fix here is to have indexes by both file and action so
1777 1779 # that when the action for a file is changed it is automatically
1778 1780 # reflected in the other action lists. But that involves a more
1779 1781 # complex data structure, so this will do for now.
1780 1782 #
1781 1783 # We don't need to do the same operation for 'dc' and 'cd' because
1782 1784 # those lists aren't consulted again.
1783 1785 mfiles.difference_update(a[0] for a in acts)
1784 1786
1785 1787 actions[ACTION_MERGE] = [a for a in actions[ACTION_MERGE]
1786 1788 if a[0] in mfiles]
1787 1789
1788 1790 progress.complete()
1789 1791 return updateresult(updated, merged, removed, unresolved)
1790 1792
1791 1793 def recordupdates(repo, actions, branchmerge):
1792 1794 "record merge actions to the dirstate"
1793 1795 # remove (must come first)
1794 1796 for f, args, msg in actions.get(ACTION_REMOVE, []):
1795 1797 if branchmerge:
1796 1798 repo.dirstate.remove(f)
1797 1799 else:
1798 1800 repo.dirstate.drop(f)
1799 1801
1800 1802 # forget (must come first)
1801 1803 for f, args, msg in actions.get(ACTION_FORGET, []):
1802 1804 repo.dirstate.drop(f)
1803 1805
1804 1806 # resolve path conflicts
1805 1807 for f, args, msg in actions.get(ACTION_PATH_CONFLICT_RESOLVE, []):
1806 1808 f0, = args
1807 1809 origf0 = repo.dirstate.copied(f0) or f0
1808 1810 repo.dirstate.add(f)
1809 1811 repo.dirstate.copy(origf0, f)
1810 1812 if f0 == origf0:
1811 1813 repo.dirstate.remove(f0)
1812 1814 else:
1813 1815 repo.dirstate.drop(f0)
1814 1816
1815 1817 # re-add
1816 1818 for f, args, msg in actions.get(ACTION_ADD, []):
1817 1819 repo.dirstate.add(f)
1818 1820
1819 1821 # re-add/mark as modified
1820 1822 for f, args, msg in actions.get(ACTION_ADD_MODIFIED, []):
1821 1823 if branchmerge:
1822 1824 repo.dirstate.normallookup(f)
1823 1825 else:
1824 1826 repo.dirstate.add(f)
1825 1827
1826 1828 # exec change
1827 1829 for f, args, msg in actions.get(ACTION_EXEC, []):
1828 1830 repo.dirstate.normallookup(f)
1829 1831
1830 1832 # keep
1831 1833 for f, args, msg in actions.get(ACTION_KEEP, []):
1832 1834 pass
1833 1835
1834 1836 # get
1835 1837 for f, args, msg in actions.get(ACTION_GET, []):
1836 1838 if branchmerge:
1837 1839 repo.dirstate.otherparent(f)
1838 1840 else:
1839 1841 repo.dirstate.normal(f)
1840 1842
1841 1843 # merge
1842 1844 for f, args, msg in actions.get(ACTION_MERGE, []):
1843 1845 f1, f2, fa, move, anc = args
1844 1846 if branchmerge:
1845 1847 # We've done a branch merge, mark this file as merged
1846 1848 # so that we properly record the merger later
1847 1849 repo.dirstate.merge(f)
1848 1850 if f1 != f2: # copy/rename
1849 1851 if move:
1850 1852 repo.dirstate.remove(f1)
1851 1853 if f1 != f:
1852 1854 repo.dirstate.copy(f1, f)
1853 1855 else:
1854 1856 repo.dirstate.copy(f2, f)
1855 1857 else:
1856 1858 # We've update-merged a locally modified file, so
1857 1859 # we set the dirstate to emulate a normal checkout
1858 1860 # of that file some time in the past. Thus our
1859 1861 # merge will appear as a normal local file
1860 1862 # modification.
1861 1863 if f2 == f: # file not locally copied/moved
1862 1864 repo.dirstate.normallookup(f)
1863 1865 if move:
1864 1866 repo.dirstate.drop(f1)
1865 1867
1866 1868 # directory rename, move local
1867 1869 for f, args, msg in actions.get(ACTION_DIR_RENAME_MOVE_LOCAL, []):
1868 1870 f0, flag = args
1869 1871 if branchmerge:
1870 1872 repo.dirstate.add(f)
1871 1873 repo.dirstate.remove(f0)
1872 1874 repo.dirstate.copy(f0, f)
1873 1875 else:
1874 1876 repo.dirstate.normal(f)
1875 1877 repo.dirstate.drop(f0)
1876 1878
1877 1879 # directory rename, get
1878 1880 for f, args, msg in actions.get(ACTION_LOCAL_DIR_RENAME_GET, []):
1879 1881 f0, flag = args
1880 1882 if branchmerge:
1881 1883 repo.dirstate.add(f)
1882 1884 repo.dirstate.copy(f0, f)
1883 1885 else:
1884 1886 repo.dirstate.normal(f)
1885 1887
1886 1888 def update(repo, node, branchmerge, force, ancestor=None,
1887 1889 mergeancestor=False, labels=None, matcher=None, mergeforce=False,
1888 1890 updatecheck=None, wc=None):
1889 1891 """
1890 1892 Perform a merge between the working directory and the given node
1891 1893
1892 1894 node = the node to update to
1893 1895 branchmerge = whether to merge between branches
1894 1896 force = whether to force branch merging or file overwriting
1895 1897 matcher = a matcher to filter file lists (dirstate not updated)
1896 1898 mergeancestor = whether it is merging with an ancestor. If true,
1897 1899 we should accept the incoming changes for any prompts that occur.
1898 1900 If false, merging with an ancestor (fast-forward) is only allowed
1899 1901 between different named branches. This flag is used by rebase extension
1900 1902 as a temporary fix and should be avoided in general.
1901 1903 labels = labels to use for base, local and other
1902 1904 mergeforce = whether the merge was run with 'merge --force' (deprecated): if
1903 1905 this is True, then 'force' should be True as well.
1904 1906
1905 1907 The table below shows all the behaviors of the update command given the
1906 1908 -c/--check and -C/--clean or no options, whether the working directory is
1907 1909 dirty, whether a revision is specified, and the relationship of the parent
1908 1910 rev to the target rev (linear or not). Match from top first. The -n
1909 1911 option doesn't exist on the command line, but represents the
1910 1912 experimental.updatecheck=noconflict option.
1911 1913
1912 1914 This logic is tested by test-update-branches.t.
1913 1915
1914 1916 -c -C -n -m dirty rev linear | result
1915 1917 y y * * * * * | (1)
1916 1918 y * y * * * * | (1)
1917 1919 y * * y * * * | (1)
1918 1920 * y y * * * * | (1)
1919 1921 * y * y * * * | (1)
1920 1922 * * y y * * * | (1)
1921 1923 * * * * * n n | x
1922 1924 * * * * n * * | ok
1923 1925 n n n n y * y | merge
1924 1926 n n n n y y n | (2)
1925 1927 n n n y y * * | merge
1926 1928 n n y n y * * | merge if no conflict
1927 1929 n y n n y * * | discard
1928 1930 y n n n y * * | (3)
1929 1931
1930 1932 x = can't happen
1931 1933 * = don't-care
1932 1934 1 = incompatible options (checked in commands.py)
1933 1935 2 = abort: uncommitted changes (commit or update --clean to discard changes)
1934 1936 3 = abort: uncommitted changes (checked in commands.py)
1935 1937
1936 1938 The merge is performed inside ``wc``, a workingctx-like objects. It defaults
1937 1939 to repo[None] if None is passed.
1938 1940
1939 1941 Return the same tuple as applyupdates().
1940 1942 """
1941 1943 # Avoid cycle.
1942 1944 from . import sparse
1943 1945
1944 1946 # This function used to find the default destination if node was None, but
1945 1947 # that's now in destutil.py.
1946 1948 assert node is not None
1947 1949 if not branchmerge and not force:
1948 1950 # TODO: remove the default once all callers that pass branchmerge=False
1949 1951 # and force=False pass a value for updatecheck. We may want to allow
1950 1952 # updatecheck='abort' to better suppport some of these callers.
1951 1953 if updatecheck is None:
1952 1954 updatecheck = 'linear'
1953 1955 assert updatecheck in ('none', 'linear', 'noconflict')
1954 1956 # If we're doing a partial update, we need to skip updating
1955 1957 # the dirstate, so make a note of any partial-ness to the
1956 1958 # update here.
1957 1959 if matcher is None or matcher.always():
1958 1960 partial = False
1959 1961 else:
1960 1962 partial = True
1961 1963 with repo.wlock():
1962 1964 if wc is None:
1963 1965 wc = repo[None]
1964 1966 pl = wc.parents()
1965 1967 p1 = pl[0]
1966 1968 pas = [None]
1967 1969 if ancestor is not None:
1968 1970 pas = [repo[ancestor]]
1969 1971
1970 1972 overwrite = force and not branchmerge
1971 1973
1972 1974 p2 = repo[node]
1973 1975 if pas[0] is None:
1974 1976 if repo.ui.configlist('merge', 'preferancestor') == ['*']:
1975 1977 cahs = repo.changelog.commonancestorsheads(p1.node(), p2.node())
1976 1978 pas = [repo[anc] for anc in (sorted(cahs) or [nullid])]
1977 1979 else:
1978 1980 pas = [p1.ancestor(p2, warn=branchmerge)]
1979 1981
1980 1982 fp1, fp2, xp1, xp2 = p1.node(), p2.node(), bytes(p1), bytes(p2)
1981 1983
1982 1984 ### check phase
1983 1985 if not overwrite:
1984 1986 if len(pl) > 1:
1985 1987 raise error.Abort(_("outstanding uncommitted merge"))
1986 1988 ms = mergestate.read(repo)
1987 1989 if list(ms.unresolved()):
1988 1990 raise error.Abort(_("outstanding merge conflicts"))
1989 1991 if branchmerge:
1990 1992 if pas == [p2]:
1991 1993 raise error.Abort(_("merging with a working directory ancestor"
1992 1994 " has no effect"))
1993 1995 elif pas == [p1]:
1994 1996 if not mergeancestor and wc.branch() == p2.branch():
1995 1997 raise error.Abort(_("nothing to merge"),
1996 1998 hint=_("use 'hg update' "
1997 1999 "or check 'hg heads'"))
1998 2000 if not force and (wc.files() or wc.deleted()):
1999 2001 raise error.Abort(_("uncommitted changes"),
2000 2002 hint=_("use 'hg status' to list changes"))
2001 2003 if not wc.isinmemory():
2002 2004 for s in sorted(wc.substate):
2003 2005 wc.sub(s).bailifchanged()
2004 2006
2005 2007 elif not overwrite:
2006 2008 if p1 == p2: # no-op update
2007 2009 # call the hooks and exit early
2008 2010 repo.hook('preupdate', throw=True, parent1=xp2, parent2='')
2009 2011 repo.hook('update', parent1=xp2, parent2='', error=0)
2010 2012 return updateresult(0, 0, 0, 0)
2011 2013
2012 2014 if (updatecheck == 'linear' and
2013 2015 pas not in ([p1], [p2])): # nonlinear
2014 2016 dirty = wc.dirty(missing=True)
2015 2017 if dirty:
2016 2018 # Branching is a bit strange to ensure we do the minimal
2017 2019 # amount of call to obsutil.foreground.
2018 2020 foreground = obsutil.foreground(repo, [p1.node()])
2019 2021 # note: the <node> variable contains a random identifier
2020 2022 if repo[node].node() in foreground:
2021 2023 pass # allow updating to successors
2022 2024 else:
2023 2025 msg = _("uncommitted changes")
2024 2026 hint = _("commit or update --clean to discard changes")
2025 2027 raise error.UpdateAbort(msg, hint=hint)
2026 2028 else:
2027 2029 # Allow jumping branches if clean and specific rev given
2028 2030 pass
2029 2031
2030 2032 if overwrite:
2031 2033 pas = [wc]
2032 2034 elif not branchmerge:
2033 2035 pas = [p1]
2034 2036
2035 2037 # deprecated config: merge.followcopies
2036 2038 followcopies = repo.ui.configbool('merge', 'followcopies')
2037 2039 if overwrite:
2038 2040 followcopies = False
2039 2041 elif not pas[0]:
2040 2042 followcopies = False
2041 2043 if not branchmerge and not wc.dirty(missing=True):
2042 2044 followcopies = False
2043 2045
2044 2046 ### calculate phase
2045 2047 actionbyfile, diverge, renamedelete = calculateupdates(
2046 2048 repo, wc, p2, pas, branchmerge, force, mergeancestor,
2047 2049 followcopies, matcher=matcher, mergeforce=mergeforce)
2048 2050
2049 2051 if updatecheck == 'noconflict':
2050 2052 for f, (m, args, msg) in actionbyfile.iteritems():
2051 2053 if m not in (ACTION_GET, ACTION_KEEP, ACTION_EXEC,
2052 2054 ACTION_REMOVE, ACTION_PATH_CONFLICT_RESOLVE):
2053 2055 msg = _("conflicting changes")
2054 2056 hint = _("commit or update --clean to discard changes")
2055 2057 raise error.Abort(msg, hint=hint)
2056 2058
2057 2059 # Prompt and create actions. Most of this is in the resolve phase
2058 2060 # already, but we can't handle .hgsubstate in filemerge or
2059 2061 # subrepoutil.submerge yet so we have to keep prompting for it.
2060 2062 if '.hgsubstate' in actionbyfile:
2061 2063 f = '.hgsubstate'
2062 2064 m, args, msg = actionbyfile[f]
2063 2065 prompts = filemerge.partextras(labels)
2064 2066 prompts['f'] = f
2065 2067 if m == ACTION_CHANGED_DELETED:
2066 2068 if repo.ui.promptchoice(
2067 2069 _("local%(l)s changed %(f)s which other%(o)s deleted\n"
2068 2070 "use (c)hanged version or (d)elete?"
2069 2071 "$$ &Changed $$ &Delete") % prompts, 0):
2070 2072 actionbyfile[f] = (ACTION_REMOVE, None, 'prompt delete')
2071 2073 elif f in p1:
2072 2074 actionbyfile[f] = (ACTION_ADD_MODIFIED, None, 'prompt keep')
2073 2075 else:
2074 2076 actionbyfile[f] = (ACTION_ADD, None, 'prompt keep')
2075 2077 elif m == ACTION_DELETED_CHANGED:
2076 2078 f1, f2, fa, move, anc = args
2077 2079 flags = p2[f2].flags()
2078 2080 if repo.ui.promptchoice(
2079 2081 _("other%(o)s changed %(f)s which local%(l)s deleted\n"
2080 2082 "use (c)hanged version or leave (d)eleted?"
2081 2083 "$$ &Changed $$ &Deleted") % prompts, 0) == 0:
2082 2084 actionbyfile[f] = (ACTION_GET, (flags, False),
2083 2085 'prompt recreating')
2084 2086 else:
2085 2087 del actionbyfile[f]
2086 2088
2087 2089 # Convert to dictionary-of-lists format
2088 2090 actions = dict((m, [])
2089 2091 for m in (
2090 2092 ACTION_ADD,
2091 2093 ACTION_ADD_MODIFIED,
2092 2094 ACTION_FORGET,
2093 2095 ACTION_GET,
2094 2096 ACTION_CHANGED_DELETED,
2095 2097 ACTION_DELETED_CHANGED,
2096 2098 ACTION_REMOVE,
2097 2099 ACTION_DIR_RENAME_MOVE_LOCAL,
2098 2100 ACTION_LOCAL_DIR_RENAME_GET,
2099 2101 ACTION_MERGE,
2100 2102 ACTION_EXEC,
2101 2103 ACTION_KEEP,
2102 2104 ACTION_PATH_CONFLICT,
2103 2105 ACTION_PATH_CONFLICT_RESOLVE))
2104 2106 for f, (m, args, msg) in actionbyfile.iteritems():
2105 2107 if m not in actions:
2106 2108 actions[m] = []
2107 2109 actions[m].append((f, args, msg))
2108 2110
2109 2111 if not util.fscasesensitive(repo.path):
2110 2112 # check collision between files only in p2 for clean update
2111 2113 if (not branchmerge and
2112 2114 (force or not wc.dirty(missing=True, branch=False))):
2113 2115 _checkcollision(repo, p2.manifest(), None)
2114 2116 else:
2115 2117 _checkcollision(repo, wc.manifest(), actions)
2116 2118
2117 2119 # divergent renames
2118 2120 for f, fl in sorted(diverge.iteritems()):
2119 2121 repo.ui.warn(_("note: possible conflict - %s was renamed "
2120 2122 "multiple times to:\n") % f)
2121 2123 for nf in fl:
2122 2124 repo.ui.warn(" %s\n" % nf)
2123 2125
2124 2126 # rename and delete
2125 2127 for f, fl in sorted(renamedelete.iteritems()):
2126 2128 repo.ui.warn(_("note: possible conflict - %s was deleted "
2127 2129 "and renamed to:\n") % f)
2128 2130 for nf in fl:
2129 2131 repo.ui.warn(" %s\n" % nf)
2130 2132
2131 2133 ### apply phase
2132 2134 if not branchmerge: # just jump to the new rev
2133 2135 fp1, fp2, xp1, xp2 = fp2, nullid, xp2, ''
2134 2136 if not partial and not wc.isinmemory():
2135 2137 repo.hook('preupdate', throw=True, parent1=xp1, parent2=xp2)
2136 2138 # note that we're in the middle of an update
2137 2139 repo.vfs.write('updatestate', p2.hex())
2138 2140
2139 2141 # Advertise fsmonitor when its presence could be useful.
2140 2142 #
2141 2143 # We only advertise when performing an update from an empty working
2142 2144 # directory. This typically only occurs during initial clone.
2143 2145 #
2144 2146 # We give users a mechanism to disable the warning in case it is
2145 2147 # annoying.
2146 2148 #
2147 2149 # We only allow on Linux and MacOS because that's where fsmonitor is
2148 2150 # considered stable.
2149 2151 fsmonitorwarning = repo.ui.configbool('fsmonitor', 'warn_when_unused')
2150 2152 fsmonitorthreshold = repo.ui.configint('fsmonitor',
2151 2153 'warn_update_file_count')
2152 2154 try:
2153 2155 # avoid cycle: extensions -> cmdutil -> merge
2154 2156 from . import extensions
2155 2157 extensions.find('fsmonitor')
2156 2158 fsmonitorenabled = repo.ui.config('fsmonitor', 'mode') != 'off'
2157 2159 # We intentionally don't look at whether fsmonitor has disabled
2158 2160 # itself because a) fsmonitor may have already printed a warning
2159 2161 # b) we only care about the config state here.
2160 2162 except KeyError:
2161 2163 fsmonitorenabled = False
2162 2164
2163 2165 if (fsmonitorwarning
2164 2166 and not fsmonitorenabled
2165 2167 and p1.node() == nullid
2166 2168 and len(actions[ACTION_GET]) >= fsmonitorthreshold
2167 2169 and pycompat.sysplatform.startswith(('linux', 'darwin'))):
2168 2170 repo.ui.warn(
2169 2171 _('(warning: large working directory being used without '
2170 2172 'fsmonitor enabled; enable fsmonitor to improve performance; '
2171 2173 'see "hg help -e fsmonitor")\n'))
2172 2174
2173 2175 stats = applyupdates(repo, actions, wc, p2, overwrite, labels=labels)
2174 2176
2175 2177 if not partial and not wc.isinmemory():
2176 2178 with repo.dirstate.parentchange():
2177 2179 repo.setparents(fp1, fp2)
2178 2180 recordupdates(repo, actions, branchmerge)
2179 2181 # update completed, clear state
2180 2182 util.unlink(repo.vfs.join('updatestate'))
2181 2183
2182 2184 if not branchmerge:
2183 2185 repo.dirstate.setbranch(p2.branch())
2184 2186
2185 2187 # If we're updating to a location, clean up any stale temporary includes
2186 2188 # (ex: this happens during hg rebase --abort).
2187 2189 if not branchmerge:
2188 2190 sparse.prunetemporaryincludes(repo)
2189 2191
2190 2192 if not partial:
2191 2193 repo.hook('update', parent1=xp1, parent2=xp2,
2192 2194 error=stats.unresolvedcount)
2193 2195 return stats
2194 2196
2195 2197 def graft(repo, ctx, pctx, labels, keepparent=False):
2196 2198 """Do a graft-like merge.
2197 2199
2198 2200 This is a merge where the merge ancestor is chosen such that one
2199 2201 or more changesets are grafted onto the current changeset. In
2200 2202 addition to the merge, this fixes up the dirstate to include only
2201 2203 a single parent (if keepparent is False) and tries to duplicate any
2202 2204 renames/copies appropriately.
2203 2205
2204 2206 ctx - changeset to rebase
2205 2207 pctx - merge base, usually ctx.p1()
2206 2208 labels - merge labels eg ['local', 'graft']
2207 2209 keepparent - keep second parent if any
2208 2210
2209 2211 """
2210 2212 # If we're grafting a descendant onto an ancestor, be sure to pass
2211 2213 # mergeancestor=True to update. This does two things: 1) allows the merge if
2212 2214 # the destination is the same as the parent of the ctx (so we can use graft
2213 2215 # to copy commits), and 2) informs update that the incoming changes are
2214 2216 # newer than the destination so it doesn't prompt about "remote changed foo
2215 2217 # which local deleted".
2216 2218 mergeancestor = repo.changelog.isancestor(repo['.'].node(), ctx.node())
2217 2219
2218 2220 stats = update(repo, ctx.node(), True, True, pctx.node(),
2219 2221 mergeancestor=mergeancestor, labels=labels)
2220 2222
2221 2223 pother = nullid
2222 2224 parents = ctx.parents()
2223 2225 if keepparent and len(parents) == 2 and pctx in parents:
2224 2226 parents.remove(pctx)
2225 2227 pother = parents[0].node()
2226 2228
2227 2229 with repo.dirstate.parentchange():
2228 2230 repo.setparents(repo['.'].node(), pother)
2229 2231 repo.dirstate.write(repo.currenttransaction())
2230 2232 # fix up dirstate for copies and renames
2231 2233 copies.duplicatecopies(repo, repo[None], ctx.rev(), pctx.rev())
2232 2234 return stats
@@ -1,426 +1,434 b''
1 1 #require tic
2 2
3 3 Set up a repo
4 4
5 5 $ cp $HGRCPATH $HGRCPATH.pretest
6 6 $ cat <<EOF >> $HGRCPATH
7 7 > [ui]
8 8 > interactive = true
9 9 > interface = curses
10 10 > [experimental]
11 11 > crecordtest = testModeCommands
12 12 > EOF
13 13
14 14 Record with noeol at eof (issue5268)
15 15 $ hg init noeol
16 16 $ cd noeol
17 17 $ printf '0' > a
18 18 $ printf '0\n' > b
19 19 $ hg ci -Aqm initial
20 20 $ printf '1\n0' > a
21 21 $ printf '1\n0\n' > b
22 22 $ cat <<EOF >testModeCommands
23 23 > c
24 24 > EOF
25 25 $ HGEDITOR="\"sh\" \"`pwd`/editor.sh\"" hg commit -i -m "add hunks" -d "0 0"
26 26 $ cd ..
27 27
28 28 Normal repo
29 29 $ hg init a
30 30 $ cd a
31 31
32 32 Committing some changes but stopping on the way
33 33
34 34 $ echo "a" > a
35 35 $ hg add a
36 36 $ cat <<EOF >testModeCommands
37 37 > TOGGLE
38 38 > X
39 39 > EOF
40 40 $ hg commit -i -m "a" -d "0 0"
41 41 no changes to record
42 42 [1]
43 43 $ hg tip
44 44 changeset: -1:000000000000
45 45 tag: tip
46 46 user:
47 47 date: Thu Jan 01 00:00:00 1970 +0000
48 48
49 49
50 50 Committing some changes
51 51
52 52 $ cat <<EOF >testModeCommands
53 53 > X
54 54 > EOF
55 55 $ hg commit -i -m "a" -d "0 0"
56 56 $ hg tip
57 57 changeset: 0:cb9a9f314b8b
58 58 tag: tip
59 59 user: test
60 60 date: Thu Jan 01 00:00:00 1970 +0000
61 61 summary: a
62 62
63 63 Check that commit -i works with no changes
64 64 $ hg commit -i
65 65 no changes to record
66 66 [1]
67 67
68 68 Committing only one file
69 69
70 70 $ echo "a" >> a
71 71 >>> open('b', 'wb').write(b"1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n") and None
72 72 $ hg add b
73 73 $ cat <<EOF >testModeCommands
74 74 > TOGGLE
75 75 > KEY_DOWN
76 76 > X
77 77 > EOF
78 78 $ hg commit -i -m "one file" -d "0 0"
79 79 $ hg tip
80 80 changeset: 1:fb2705a663ea
81 81 tag: tip
82 82 user: test
83 83 date: Thu Jan 01 00:00:00 1970 +0000
84 84 summary: one file
85 85
86 86 $ hg cat -r tip a
87 87 a
88 88 $ cat a
89 89 a
90 90 a
91 91
92 92 Committing only one hunk while aborting edition of hunk
93 93
94 94 - Untoggle all the hunks, go down to the second file
95 95 - unfold it
96 96 - go down to second hunk (1 for the first hunk, 1 for the first hunkline, 1 for the second hunk, 1 for the second hunklike)
97 97 - toggle the second hunk
98 98 - toggle on and off the amend mode (to check that it toggles off)
99 99 - edit the hunk and quit the editor immediately with non-zero status
100 100 - commit
101 101
102 102 $ printf "printf 'editor ran\n'; exit 1" > editor.sh
103 103 $ echo "x" > c
104 104 $ cat b >> c
105 105 $ echo "y" >> c
106 106 $ mv c b
107 107 $ cat <<EOF >testModeCommands
108 108 > A
109 109 > KEY_DOWN
110 110 > f
111 111 > KEY_DOWN
112 112 > KEY_DOWN
113 113 > KEY_DOWN
114 114 > KEY_DOWN
115 115 > TOGGLE
116 116 > a
117 117 > a
118 118 > e
119 119 > X
120 120 > EOF
121 121 $ HGEDITOR="\"sh\" \"`pwd`/editor.sh\"" hg commit -i -m "one hunk" -d "0 0"
122 122 editor ran
123 123 $ rm editor.sh
124 124 $ hg tip
125 125 changeset: 2:7d10dfe755a8
126 126 tag: tip
127 127 user: test
128 128 date: Thu Jan 01 00:00:00 1970 +0000
129 129 summary: one hunk
130 130
131 131 $ hg cat -r tip b
132 132 1
133 133 2
134 134 3
135 135 4
136 136 5
137 137 6
138 138 7
139 139 8
140 140 9
141 141 10
142 142 y
143 143 $ cat b
144 144 x
145 145 1
146 146 2
147 147 3
148 148 4
149 149 5
150 150 6
151 151 7
152 152 8
153 153 9
154 154 10
155 155 y
156 156 $ hg commit -m "other hunks"
157 157 $ hg tip
158 158 changeset: 3:a6735021574d
159 159 tag: tip
160 160 user: test
161 161 date: Thu Jan 01 00:00:00 1970 +0000
162 162 summary: other hunks
163 163
164 164 $ hg cat -r tip b
165 165 x
166 166 1
167 167 2
168 168 3
169 169 4
170 170 5
171 171 6
172 172 7
173 173 8
174 174 9
175 175 10
176 176 y
177 177
178 178 Newly added files can be selected with the curses interface
179 179
180 180 $ hg update -C .
181 181 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
182 182 $ echo "hello" > x
183 183 $ hg add x
184 184 $ cat <<EOF >testModeCommands
185 185 > TOGGLE
186 186 > TOGGLE
187 187 > X
188 188 > EOF
189 189 $ hg st
190 190 A x
191 191 ? testModeCommands
192 192 $ hg commit -i -m "newly added file" -d "0 0"
193 193 $ hg st
194 194 ? testModeCommands
195 195
196 196 Amend option works
197 197 $ echo "hello world" > x
198 198 $ hg diff -c .
199 199 diff -r a6735021574d -r 2b0e9be4d336 x
200 200 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
201 201 +++ b/x Thu Jan 01 00:00:00 1970 +0000
202 202 @@ -0,0 +1,1 @@
203 203 +hello
204 204 $ cat <<EOF >testModeCommands
205 205 > a
206 206 > X
207 207 > EOF
208 208 $ hg commit -i -m "newly added file" -d "0 0"
209 209 saved backup bundle to $TESTTMP/a/.hg/strip-backup/2b0e9be4d336-3cf0bc8c-amend.hg
210 210 $ hg diff -c .
211 211 diff -r a6735021574d -r c1d239d165ae x
212 212 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
213 213 +++ b/x Thu Jan 01 00:00:00 1970 +0000
214 214 @@ -0,0 +1,1 @@
215 215 +hello world
216 216
217 Make file empty
218 $ printf "" > x
219 $ cat <<EOF >testModeCommands
220 > X
221 > EOF
222 $ hg ci -i -m emptify -d "0 0"
223 $ hg update -C '.^' -q
224
217 225 Editing a hunk puts you back on that hunk when done editing (issue5041)
218 226 To do that, we change two lines in a file, pretend to edit the second line,
219 227 exit, toggle the line selected at the end of the edit and commit.
220 228 The first line should be recorded if we were put on the second line at the end
221 229 of the edit.
222 230
223 231 $ hg update -C .
224 232 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
225 233 $ echo "foo" > x
226 234 $ echo "hello world" >> x
227 235 $ echo "bar" >> x
228 236 $ cat <<EOF >testModeCommands
229 237 > f
230 238 > KEY_DOWN
231 239 > KEY_DOWN
232 240 > KEY_DOWN
233 241 > KEY_DOWN
234 242 > e
235 243 > TOGGLE
236 244 > X
237 245 > EOF
238 246 $ printf "printf 'editor ran\n'; exit 0" > editor.sh
239 $ HGEDITOR="\"sh\" \"`pwd`/editor.sh\"" hg commit -i -m "edit hunk" -d "0 0"
247 $ HGEDITOR="\"sh\" \"`pwd`/editor.sh\"" hg commit -i -m "edit hunk" -d "0 0" -q
240 248 editor ran
241 249 $ hg cat -r . x
242 250 foo
243 251 hello world
244 252
245 253 Testing the review option. The entire final filtered patch should show
246 254 up in the editor and be editable. We will unselect the second file and
247 255 the first hunk of the third file. During review, we will decide that
248 256 "lower" sounds better than "bottom", and the final commit should
249 257 reflect this edition.
250 258
251 259 $ hg update -C .
252 260 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
253 261 $ echo "top" > c
254 262 $ cat x >> c
255 263 $ echo "bottom" >> c
256 264 $ mv c x
257 265 $ echo "third a" >> a
258 266 $ echo "we will unselect this" >> b
259 267
260 268 $ cat > editor.sh <<EOF
261 269 > cat "\$1"
262 270 > cat "\$1" | sed s/bottom/lower/ > tmp
263 271 > mv tmp "\$1"
264 272 > EOF
265 273 $ cat > testModeCommands <<EOF
266 274 > KEY_DOWN
267 275 > TOGGLE
268 276 > KEY_DOWN
269 277 > f
270 278 > KEY_DOWN
271 279 > TOGGLE
272 280 > R
273 281 > EOF
274 282
275 283 $ HGEDITOR="\"sh\" \"`pwd`/editor.sh\"" hg commit -i -m "review hunks" -d "0 0"
276 284 # To remove '-' lines, make them ' ' lines (context).
277 285 # To remove '+' lines, delete them.
278 286 # Lines starting with # will be removed from the patch.
279 287 #
280 288 # If the patch applies cleanly, the edited patch will immediately
281 289 # be finalised. If it does not apply cleanly, rejects files will be
282 290 # generated. You can use those when you try again.
283 291 diff --git a/a b/a
284 292 --- a/a
285 293 +++ b/a
286 294 @@ -1,2 +1,3 @@
287 295 a
288 296 a
289 297 +third a
290 298 diff --git a/x b/x
291 299 --- a/x
292 300 +++ b/x
293 301 @@ -1,2 +1,3 @@
294 302 foo
295 303 hello world
296 304 +bottom
297 305
298 306 $ hg cat -r . a
299 307 a
300 308 a
301 309 third a
302 310
303 311 $ hg cat -r . b
304 312 x
305 313 1
306 314 2
307 315 3
308 316 4
309 317 5
310 318 6
311 319 7
312 320 8
313 321 9
314 322 10
315 323 y
316 324
317 325 $ hg cat -r . x
318 326 foo
319 327 hello world
320 328 lower
321 329
322 330 Check spacemovesdown
323 331
324 332 $ cat <<EOF >> $HGRCPATH
325 333 > [experimental]
326 334 > spacemovesdown = true
327 335 > EOF
328 336 $ cat <<EOF >testModeCommands
329 337 > TOGGLE
330 338 > TOGGLE
331 339 > X
332 340 > EOF
333 341 $ hg status -q
334 342 M b
335 343 M x
336 344 $ hg commit -i -m "nothing to commit?" -d "0 0"
337 345 no changes to record
338 346 [1]
339 347
340 348 Check ui.interface logic for the chunkselector
341 349
342 350 The default interface is text
343 351 $ cp $HGRCPATH.pretest $HGRCPATH
344 352 $ chunkselectorinterface() {
345 353 > $PYTHON <<EOF
346 354 > from mercurial import hg, ui;\
347 355 > repo = hg.repository(ui.ui.load(), ".");\
348 356 > print(repo.ui.interface("chunkselector"))
349 357 > EOF
350 358 > }
351 359 $ chunkselectorinterface
352 360 text
353 361
354 362 If only the default is set, we'll use that for the feature, too
355 363 $ cp $HGRCPATH.pretest $HGRCPATH
356 364 $ cat <<EOF >> $HGRCPATH
357 365 > [ui]
358 366 > interface = curses
359 367 > EOF
360 368 $ chunkselectorinterface
361 369 curses
362 370
363 371 It is possible to override the default interface with a feature specific
364 372 interface
365 373 $ cp $HGRCPATH.pretest $HGRCPATH
366 374 $ cat <<EOF >> $HGRCPATH
367 375 > [ui]
368 376 > interface = text
369 377 > interface.chunkselector = curses
370 378 > EOF
371 379
372 380 $ chunkselectorinterface
373 381 curses
374 382
375 383 $ cp $HGRCPATH.pretest $HGRCPATH
376 384 $ cat <<EOF >> $HGRCPATH
377 385 > [ui]
378 386 > interface = curses
379 387 > interface.chunkselector = text
380 388 > EOF
381 389
382 390 $ chunkselectorinterface
383 391 text
384 392
385 393 If a bad interface name is given, we use the default value (with a nice
386 394 error message to suggest that the configuration needs to be fixed)
387 395
388 396 $ cp $HGRCPATH.pretest $HGRCPATH
389 397 $ cat <<EOF >> $HGRCPATH
390 398 > [ui]
391 399 > interface = blah
392 400 > EOF
393 401 $ chunkselectorinterface
394 402 invalid value for ui.interface: blah (using text)
395 403 text
396 404
397 405 $ cp $HGRCPATH.pretest $HGRCPATH
398 406 $ cat <<EOF >> $HGRCPATH
399 407 > [ui]
400 408 > interface = curses
401 409 > interface.chunkselector = blah
402 410 > EOF
403 411 $ chunkselectorinterface
404 412 invalid value for ui.interface.chunkselector: blah (using curses)
405 413 curses
406 414
407 415 $ cp $HGRCPATH.pretest $HGRCPATH
408 416 $ cat <<EOF >> $HGRCPATH
409 417 > [ui]
410 418 > interface = blah
411 419 > interface.chunkselector = curses
412 420 > EOF
413 421 $ chunkselectorinterface
414 422 invalid value for ui.interface: blah
415 423 curses
416 424
417 425 $ cp $HGRCPATH.pretest $HGRCPATH
418 426 $ cat <<EOF >> $HGRCPATH
419 427 > [ui]
420 428 > interface = blah
421 429 > interface.chunkselector = blah
422 430 > EOF
423 431 $ chunkselectorinterface
424 432 invalid value for ui.interface: blah
425 433 invalid value for ui.interface.chunkselector: blah (using text)
426 434 text
@@ -1,1943 +1,1983 b''
1 1 Let commit recurse into subrepos by default to match pre-2.0 behavior:
2 2
3 3 $ echo "[ui]" >> $HGRCPATH
4 4 $ echo "commitsubrepos = Yes" >> $HGRCPATH
5 5
6 6 $ hg init t
7 7 $ cd t
8 8
9 9 first revision, no sub
10 10
11 11 $ echo a > a
12 12 $ hg ci -Am0
13 13 adding a
14 14
15 15 add first sub
16 16
17 17 $ echo s = s > .hgsub
18 18 $ hg add .hgsub
19 19 $ hg init s
20 20 $ echo a > s/a
21 21
22 22 Issue2232: committing a subrepo without .hgsub
23 23
24 24 $ hg ci -mbad s
25 25 abort: can't commit subrepos without .hgsub
26 26 [255]
27 27
28 28 $ hg -R s add s/a
29 29 $ hg files -S
30 30 .hgsub
31 31 a
32 32 s/a
33 33
34 34 $ hg -R s ci -Ams0
35 35 $ hg sum
36 36 parent: 0:f7b1eb17ad24 tip
37 37 0
38 38 branch: default
39 39 commit: 1 added, 1 subrepos
40 40 update: (current)
41 41 phases: 1 draft
42 42 $ hg ci -m1
43 43
44 44 test handling .hgsubstate "added" explicitly.
45 45
46 46 $ hg parents --template '{node}\n{files}\n'
47 47 7cf8cfea66e410e8e3336508dfeec07b3192de51
48 48 .hgsub .hgsubstate
49 49 $ hg rollback -q
50 50 $ hg add .hgsubstate
51 51 $ hg ci -m1
52 52 $ hg parents --template '{node}\n{files}\n'
53 53 7cf8cfea66e410e8e3336508dfeec07b3192de51
54 54 .hgsub .hgsubstate
55 55
56 56 Subrepopath which overlaps with filepath, does not change warnings in remove()
57 57
58 58 $ mkdir snot
59 59 $ touch snot/file
60 60 $ hg remove -S snot/file
61 61 not removing snot/file: file is untracked
62 62 [1]
63 63 $ hg cat snot/filenot
64 64 snot/filenot: no such file in rev 7cf8cfea66e4
65 65 [1]
66 66 $ rm -r snot
67 67
68 68 Revert subrepo and test subrepo fileset keyword:
69 69
70 70 $ echo b > s/a
71 71 $ hg revert --dry-run "set:subrepo('glob:s*')"
72 72 reverting subrepo s
73 73 reverting s/a
74 74 $ cat s/a
75 75 b
76 76 $ hg revert "set:subrepo('glob:s*')"
77 77 reverting subrepo s
78 78 reverting s/a
79 79 $ cat s/a
80 80 a
81 81 $ rm s/a.orig
82 82
83 83 Revert subrepo with no backup. The "reverting s/a" line is gone since
84 84 we're really running 'hg update' in the subrepo:
85 85
86 86 $ echo b > s/a
87 87 $ hg revert --no-backup s
88 88 reverting subrepo s
89 89
90 90 Issue2022: update -C
91 91
92 92 $ echo b > s/a
93 93 $ hg sum
94 94 parent: 1:7cf8cfea66e4 tip
95 95 1
96 96 branch: default
97 97 commit: 1 subrepos
98 98 update: (current)
99 99 phases: 2 draft
100 100 $ hg co -C 1
101 101 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
102 102 $ hg sum
103 103 parent: 1:7cf8cfea66e4 tip
104 104 1
105 105 branch: default
106 106 commit: (clean)
107 107 update: (current)
108 108 phases: 2 draft
109 109
110 110 commands that require a clean repo should respect subrepos
111 111
112 112 $ echo b >> s/a
113 113 $ hg backout tip
114 114 abort: uncommitted changes in subrepository "s"
115 115 [255]
116 116 $ hg revert -C -R s s/a
117 117
118 118 add sub sub
119 119
120 120 $ echo ss = ss > s/.hgsub
121 121 $ hg init s/ss
122 122 $ echo a > s/ss/a
123 123 $ hg -R s add s/.hgsub
124 124 $ hg -R s/ss add s/ss/a
125 125 $ hg sum
126 126 parent: 1:7cf8cfea66e4 tip
127 127 1
128 128 branch: default
129 129 commit: 1 subrepos
130 130 update: (current)
131 131 phases: 2 draft
132 132 $ hg ci -m2
133 133 committing subrepository s
134 134 committing subrepository s/ss
135 135 $ hg sum
136 136 parent: 2:df30734270ae tip
137 137 2
138 138 branch: default
139 139 commit: (clean)
140 140 update: (current)
141 141 phases: 3 draft
142 142
143 143 test handling .hgsubstate "modified" explicitly.
144 144
145 145 $ hg parents --template '{node}\n{files}\n'
146 146 df30734270ae757feb35e643b7018e818e78a9aa
147 147 .hgsubstate
148 148 $ hg rollback -q
149 149 $ hg status -A .hgsubstate
150 150 M .hgsubstate
151 151 $ hg ci -m2
152 152 $ hg parents --template '{node}\n{files}\n'
153 153 df30734270ae757feb35e643b7018e818e78a9aa
154 154 .hgsubstate
155 155
156 156 bump sub rev (and check it is ignored by ui.commitsubrepos)
157 157
158 158 $ echo b > s/a
159 159 $ hg -R s ci -ms1
160 160 $ hg --config ui.commitsubrepos=no ci -m3
161 161
162 162 leave sub dirty (and check ui.commitsubrepos=no aborts the commit)
163 163
164 164 $ echo c > s/a
165 165 $ hg --config ui.commitsubrepos=no ci -m4
166 166 abort: uncommitted changes in subrepository "s"
167 167 (use --subrepos for recursive commit)
168 168 [255]
169 169 $ hg id
170 170 f6affe3fbfaa+ tip
171 171 $ hg -R s ci -mc
172 172 $ hg id
173 173 f6affe3fbfaa+ tip
174 174 $ echo d > s/a
175 175 $ hg ci -m4
176 176 committing subrepository s
177 177 $ hg tip -R s
178 178 changeset: 4:02dcf1d70411
179 179 tag: tip
180 180 user: test
181 181 date: Thu Jan 01 00:00:00 1970 +0000
182 182 summary: 4
183 183
184 184
185 185 check caching
186 186
187 187 $ hg co 0
188 188 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
189 189 $ hg debugsub
190 190
191 191 restore
192 192
193 193 $ hg co
194 194 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
195 195 $ hg debugsub
196 196 path s
197 197 source s
198 198 revision 02dcf1d704118aee3ee306ccfa1910850d5b05ef
199 199
200 200 new branch for merge tests
201 201
202 202 $ hg co 1
203 203 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
204 204 $ echo t = t >> .hgsub
205 205 $ hg init t
206 206 $ echo t > t/t
207 207 $ hg -R t add t
208 208 adding t/t
209 209
210 210 5
211 211
212 212 $ hg ci -m5 # add sub
213 213 committing subrepository t
214 214 created new head
215 215 $ echo t2 > t/t
216 216
217 217 6
218 218
219 219 $ hg st -R s
220 220 $ hg ci -m6 # change sub
221 221 committing subrepository t
222 222 $ hg debugsub
223 223 path s
224 224 source s
225 225 revision e4ece1bf43360ddc8f6a96432201a37b7cd27ae4
226 226 path t
227 227 source t
228 228 revision 6747d179aa9a688023c4b0cad32e4c92bb7f34ad
229 229 $ echo t3 > t/t
230 230
231 231 7
232 232
233 233 $ hg ci -m7 # change sub again for conflict test
234 234 committing subrepository t
235 235 $ hg rm .hgsub
236 236
237 237 8
238 238
239 239 $ hg ci -m8 # remove sub
240 240
241 241 test handling .hgsubstate "removed" explicitly.
242 242
243 243 $ hg parents --template '{node}\n{files}\n'
244 244 96615c1dad2dc8e3796d7332c77ce69156f7b78e
245 245 .hgsub .hgsubstate
246 246 $ hg rollback -q
247 247 $ hg remove .hgsubstate
248 248 $ hg ci -m8
249 249 $ hg parents --template '{node}\n{files}\n'
250 250 96615c1dad2dc8e3796d7332c77ce69156f7b78e
251 251 .hgsub .hgsubstate
252 252
253 253 merge tests
254 254
255 255 $ hg co -C 3
256 256 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
257 257 $ hg merge 5 # test adding
258 258 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
259 259 (branch merge, don't forget to commit)
260 260 $ hg debugsub
261 261 path s
262 262 source s
263 263 revision fc627a69481fcbe5f1135069e8a3881c023e4cf5
264 264 path t
265 265 source t
266 266 revision 60ca1237c19474e7a3978b0dc1ca4e6f36d51382
267 267 $ hg ci -m9
268 268 created new head
269 269 $ hg merge 6 --debug # test change
270 270 searching for copies back to rev 2
271 271 resolving manifests
272 272 branchmerge: True, force: False, partial: False
273 273 ancestor: 1f14a2e2d3ec, local: f0d2028bf86d+, remote: 1831e14459c4
274 274 starting 4 threads for background file closing (?)
275 275 .hgsubstate: versions differ -> m (premerge)
276 276 subrepo merge f0d2028bf86d+ 1831e14459c4 1f14a2e2d3ec
277 277 subrepo t: other changed, get t:6747d179aa9a688023c4b0cad32e4c92bb7f34ad:hg
278 278 getting subrepo t
279 279 resolving manifests
280 280 branchmerge: False, force: False, partial: False
281 281 ancestor: 60ca1237c194, local: 60ca1237c194+, remote: 6747d179aa9a
282 282 t: remote is newer -> g
283 283 getting t
284 284 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
285 285 (branch merge, don't forget to commit)
286 286 $ hg debugsub
287 287 path s
288 288 source s
289 289 revision fc627a69481fcbe5f1135069e8a3881c023e4cf5
290 290 path t
291 291 source t
292 292 revision 6747d179aa9a688023c4b0cad32e4c92bb7f34ad
293 293 $ echo conflict > t/t
294 294 $ hg ci -m10
295 295 committing subrepository t
296 296 $ HGMERGE=internal:merge hg merge --debug 7 # test conflict
297 297 searching for copies back to rev 2
298 298 resolving manifests
299 299 branchmerge: True, force: False, partial: False
300 300 ancestor: 1831e14459c4, local: e45c8b14af55+, remote: f94576341bcf
301 301 starting 4 threads for background file closing (?)
302 302 .hgsubstate: versions differ -> m (premerge)
303 303 subrepo merge e45c8b14af55+ f94576341bcf 1831e14459c4
304 304 subrepo t: both sides changed
305 305 subrepository t diverged (local revision: 20a0db6fbf6c, remote revision: 7af322bc1198)
306 306 starting 4 threads for background file closing (?)
307 307 (M)erge, keep (l)ocal [working copy] or keep (r)emote [merge rev]? m
308 308 merging subrepository "t"
309 309 searching for copies back to rev 2
310 310 resolving manifests
311 311 branchmerge: True, force: False, partial: False
312 312 ancestor: 6747d179aa9a, local: 20a0db6fbf6c+, remote: 7af322bc1198
313 313 preserving t for resolve of t
314 314 starting 4 threads for background file closing (?)
315 315 t: versions differ -> m (premerge)
316 316 picked tool ':merge' for t (binary False symlink False changedelete False)
317 317 merging t
318 318 my t@20a0db6fbf6c+ other t@7af322bc1198 ancestor t@6747d179aa9a
319 319 t: versions differ -> m (merge)
320 320 picked tool ':merge' for t (binary False symlink False changedelete False)
321 321 my t@20a0db6fbf6c+ other t@7af322bc1198 ancestor t@6747d179aa9a
322 322 warning: conflicts while merging t! (edit, then use 'hg resolve --mark')
323 323 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
324 324 use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
325 325 subrepo t: merge with t:7af322bc1198a32402fe903e0b7ebcfc5c9bf8f4:hg
326 326 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
327 327 (branch merge, don't forget to commit)
328 328
329 329 should conflict
330 330
331 331 $ cat t/t
332 332 <<<<<<< local: 20a0db6fbf6c - test: 10
333 333 conflict
334 334 =======
335 335 t3
336 336 >>>>>>> other: 7af322bc1198 - test: 7
337 337
338 338 11: remove subrepo t
339 339
340 340 $ hg co -C 5
341 341 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
342 342 $ hg revert -r 4 .hgsub # remove t
343 343 $ hg ci -m11
344 344 created new head
345 345 $ hg debugsub
346 346 path s
347 347 source s
348 348 revision e4ece1bf43360ddc8f6a96432201a37b7cd27ae4
349 349
350 350 local removed, remote changed, keep changed
351 351
352 352 $ hg merge 6
353 353 remote [merge rev] changed subrepository t which local [working copy] removed
354 354 use (c)hanged version or (d)elete? c
355 355 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
356 356 (branch merge, don't forget to commit)
357 357 BROKEN: should include subrepo t
358 358 $ hg debugsub
359 359 path s
360 360 source s
361 361 revision e4ece1bf43360ddc8f6a96432201a37b7cd27ae4
362 362 $ cat .hgsubstate
363 363 e4ece1bf43360ddc8f6a96432201a37b7cd27ae4 s
364 364 6747d179aa9a688023c4b0cad32e4c92bb7f34ad t
365 365 $ hg ci -m 'local removed, remote changed, keep changed'
366 366 BROKEN: should include subrepo t
367 367 $ hg debugsub
368 368 path s
369 369 source s
370 370 revision e4ece1bf43360ddc8f6a96432201a37b7cd27ae4
371 371 BROKEN: should include subrepo t
372 372 $ cat .hgsubstate
373 373 e4ece1bf43360ddc8f6a96432201a37b7cd27ae4 s
374 374 $ cat t/t
375 375 t2
376 376
377 377 local removed, remote changed, keep removed
378 378
379 379 $ hg co -C 11
380 380 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
381 381 $ hg merge --config ui.interactive=true 6 <<EOF
382 382 > d
383 383 > EOF
384 384 remote [merge rev] changed subrepository t which local [working copy] removed
385 385 use (c)hanged version or (d)elete? d
386 386 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
387 387 (branch merge, don't forget to commit)
388 388 $ hg debugsub
389 389 path s
390 390 source s
391 391 revision e4ece1bf43360ddc8f6a96432201a37b7cd27ae4
392 392 $ cat .hgsubstate
393 393 e4ece1bf43360ddc8f6a96432201a37b7cd27ae4 s
394 394 $ hg ci -m 'local removed, remote changed, keep removed'
395 395 created new head
396 396 $ hg debugsub
397 397 path s
398 398 source s
399 399 revision e4ece1bf43360ddc8f6a96432201a37b7cd27ae4
400 400 $ cat .hgsubstate
401 401 e4ece1bf43360ddc8f6a96432201a37b7cd27ae4 s
402 402
403 403 local changed, remote removed, keep changed
404 404
405 405 $ hg co -C 6
406 406 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
407 407 $ hg merge 11
408 408 local [working copy] changed subrepository t which remote [merge rev] removed
409 409 use (c)hanged version or (d)elete? c
410 410 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
411 411 (branch merge, don't forget to commit)
412 412 BROKEN: should include subrepo t
413 413 $ hg debugsub
414 414 path s
415 415 source s
416 416 revision e4ece1bf43360ddc8f6a96432201a37b7cd27ae4
417 417 BROKEN: should include subrepo t
418 418 $ cat .hgsubstate
419 419 e4ece1bf43360ddc8f6a96432201a37b7cd27ae4 s
420 420 $ hg ci -m 'local changed, remote removed, keep changed'
421 421 created new head
422 422 BROKEN: should include subrepo t
423 423 $ hg debugsub
424 424 path s
425 425 source s
426 426 revision e4ece1bf43360ddc8f6a96432201a37b7cd27ae4
427 427 BROKEN: should include subrepo t
428 428 $ cat .hgsubstate
429 429 e4ece1bf43360ddc8f6a96432201a37b7cd27ae4 s
430 430 $ cat t/t
431 431 t2
432 432
433 433 local changed, remote removed, keep removed
434 434
435 435 $ hg co -C 6
436 436 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
437 437 $ hg merge --config ui.interactive=true 11 <<EOF
438 438 > d
439 439 > EOF
440 440 local [working copy] changed subrepository t which remote [merge rev] removed
441 441 use (c)hanged version or (d)elete? d
442 442 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
443 443 (branch merge, don't forget to commit)
444 444 $ hg debugsub
445 445 path s
446 446 source s
447 447 revision e4ece1bf43360ddc8f6a96432201a37b7cd27ae4
448 448 $ cat .hgsubstate
449 449 e4ece1bf43360ddc8f6a96432201a37b7cd27ae4 s
450 450 $ hg ci -m 'local changed, remote removed, keep removed'
451 451 created new head
452 452 $ hg debugsub
453 453 path s
454 454 source s
455 455 revision e4ece1bf43360ddc8f6a96432201a37b7cd27ae4
456 456 $ cat .hgsubstate
457 457 e4ece1bf43360ddc8f6a96432201a37b7cd27ae4 s
458 458
459 459 clean up to avoid having to fix up the tests below
460 460
461 461 $ hg co -C 10
462 462 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
463 463 $ cat >> $HGRCPATH <<EOF
464 464 > [extensions]
465 465 > strip=
466 466 > EOF
467 467 $ hg strip -r 11:15
468 468 saved backup bundle to $TESTTMP/t/.hg/strip-backup/*-backup.hg (glob)
469 469
470 470 clone
471 471
472 472 $ cd ..
473 473 $ hg clone t tc
474 474 updating to branch default
475 475 cloning subrepo s from $TESTTMP/t/s
476 476 cloning subrepo s/ss from $TESTTMP/t/s/ss
477 477 cloning subrepo t from $TESTTMP/t/t
478 478 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
479 479 $ cd tc
480 480 $ hg debugsub
481 481 path s
482 482 source s
483 483 revision fc627a69481fcbe5f1135069e8a3881c023e4cf5
484 484 path t
485 485 source t
486 486 revision 20a0db6fbf6c3d2836e6519a642ae929bfc67c0e
487 487 $ cd ..
488 488
489 489 clone with subrepo disabled (update should fail)
490 490
491 491 $ hg clone t -U tc2 --config subrepos.allowed=false
492 492 $ hg update -R tc2 --config subrepos.allowed=false
493 493 abort: subrepos not enabled
494 494 (see 'hg help config.subrepos' for details)
495 495 [255]
496 496 $ ls tc2
497 497 a
498 498
499 499 $ hg clone t tc3 --config subrepos.allowed=false
500 500 updating to branch default
501 501 abort: subrepos not enabled
502 502 (see 'hg help config.subrepos' for details)
503 503 [255]
504 504 $ ls tc3
505 505 a
506 506
507 507 And again with just the hg type disabled
508 508
509 509 $ hg clone t -U tc4 --config subrepos.hg:allowed=false
510 510 $ hg update -R tc4 --config subrepos.hg:allowed=false
511 511 abort: hg subrepos not allowed
512 512 (see 'hg help config.subrepos' for details)
513 513 [255]
514 514 $ ls tc4
515 515 a
516 516
517 517 $ hg clone t tc5 --config subrepos.hg:allowed=false
518 518 updating to branch default
519 519 abort: hg subrepos not allowed
520 520 (see 'hg help config.subrepos' for details)
521 521 [255]
522 522 $ ls tc5
523 523 a
524 524
525 525 push
526 526
527 527 $ cd tc
528 528 $ echo bah > t/t
529 529 $ hg ci -m11
530 530 committing subrepository t
531 531 $ hg push
532 532 pushing to $TESTTMP/t
533 533 no changes made to subrepo s/ss since last push to $TESTTMP/t/s/ss
534 534 no changes made to subrepo s since last push to $TESTTMP/t/s
535 535 pushing subrepo t to $TESTTMP/t/t
536 536 searching for changes
537 537 adding changesets
538 538 adding manifests
539 539 adding file changes
540 540 added 1 changesets with 1 changes to 1 files
541 541 searching for changes
542 542 adding changesets
543 543 adding manifests
544 544 adding file changes
545 545 added 1 changesets with 1 changes to 1 files
546 546
547 547 push -f
548 548
549 549 $ echo bah > s/a
550 550 $ hg ci -m12
551 551 committing subrepository s
552 552 $ hg push
553 553 pushing to $TESTTMP/t
554 554 no changes made to subrepo s/ss since last push to $TESTTMP/t/s/ss
555 555 pushing subrepo s to $TESTTMP/t/s
556 556 searching for changes
557 557 abort: push creates new remote head 12a213df6fa9! (in subrepository "s")
558 558 (merge or see 'hg help push' for details about pushing new heads)
559 559 [255]
560 560 $ hg push -f
561 561 pushing to $TESTTMP/t
562 562 pushing subrepo s/ss to $TESTTMP/t/s/ss
563 563 searching for changes
564 564 no changes found
565 565 pushing subrepo s to $TESTTMP/t/s
566 566 searching for changes
567 567 adding changesets
568 568 adding manifests
569 569 adding file changes
570 570 added 1 changesets with 1 changes to 1 files (+1 heads)
571 571 pushing subrepo t to $TESTTMP/t/t
572 572 searching for changes
573 573 no changes found
574 574 searching for changes
575 575 adding changesets
576 576 adding manifests
577 577 adding file changes
578 578 added 1 changesets with 1 changes to 1 files
579 579
580 580 check that unmodified subrepos are not pushed
581 581
582 582 $ hg clone . ../tcc
583 583 updating to branch default
584 584 cloning subrepo s from $TESTTMP/tc/s
585 585 cloning subrepo s/ss from $TESTTMP/tc/s/ss
586 586 cloning subrepo t from $TESTTMP/tc/t
587 587 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
588 588
589 589 the subrepos on the new clone have nothing to push to its source
590 590
591 591 $ hg push -R ../tcc .
592 592 pushing to .
593 593 no changes made to subrepo s/ss since last push to s/ss
594 594 no changes made to subrepo s since last push to s
595 595 no changes made to subrepo t since last push to t
596 596 searching for changes
597 597 no changes found
598 598 [1]
599 599
600 600 the subrepos on the source do not have a clean store versus the clone target
601 601 because they were never explicitly pushed to the source
602 602
603 603 $ hg push ../tcc
604 604 pushing to ../tcc
605 605 pushing subrepo s/ss to ../tcc/s/ss
606 606 searching for changes
607 607 no changes found
608 608 pushing subrepo s to ../tcc/s
609 609 searching for changes
610 610 no changes found
611 611 pushing subrepo t to ../tcc/t
612 612 searching for changes
613 613 no changes found
614 614 searching for changes
615 615 no changes found
616 616 [1]
617 617
618 618 after push their stores become clean
619 619
620 620 $ hg push ../tcc
621 621 pushing to ../tcc
622 622 no changes made to subrepo s/ss since last push to ../tcc/s/ss
623 623 no changes made to subrepo s since last push to ../tcc/s
624 624 no changes made to subrepo t since last push to ../tcc/t
625 625 searching for changes
626 626 no changes found
627 627 [1]
628 628
629 629 updating a subrepo to a different revision or changing
630 630 its working directory does not make its store dirty
631 631
632 632 $ hg -R s update '.^'
633 633 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
634 634 $ hg push
635 635 pushing to $TESTTMP/t
636 636 no changes made to subrepo s/ss since last push to $TESTTMP/t/s/ss
637 637 no changes made to subrepo s since last push to $TESTTMP/t/s
638 638 no changes made to subrepo t since last push to $TESTTMP/t/t
639 639 searching for changes
640 640 no changes found
641 641 [1]
642 642 $ echo foo >> s/a
643 643 $ hg push
644 644 pushing to $TESTTMP/t
645 645 no changes made to subrepo s/ss since last push to $TESTTMP/t/s/ss
646 646 no changes made to subrepo s since last push to $TESTTMP/t/s
647 647 no changes made to subrepo t since last push to $TESTTMP/t/t
648 648 searching for changes
649 649 no changes found
650 650 [1]
651 651 $ hg -R s update -C tip
652 652 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
653 653
654 654 committing into a subrepo makes its store (but not its parent's store) dirty
655 655
656 656 $ echo foo >> s/ss/a
657 657 $ hg -R s/ss commit -m 'test dirty store detection'
658 658
659 659 $ hg out -S -r `hg log -r tip -T "{node|short}"`
660 660 comparing with $TESTTMP/t
661 661 searching for changes
662 662 no changes found
663 663 comparing with $TESTTMP/t/s
664 664 searching for changes
665 665 no changes found
666 666 comparing with $TESTTMP/t/s/ss
667 667 searching for changes
668 668 changeset: 1:79ea5566a333
669 669 tag: tip
670 670 user: test
671 671 date: Thu Jan 01 00:00:00 1970 +0000
672 672 summary: test dirty store detection
673 673
674 674 comparing with $TESTTMP/t/t
675 675 searching for changes
676 676 no changes found
677 677
678 678 $ hg push
679 679 pushing to $TESTTMP/t
680 680 pushing subrepo s/ss to $TESTTMP/t/s/ss
681 681 searching for changes
682 682 adding changesets
683 683 adding manifests
684 684 adding file changes
685 685 added 1 changesets with 1 changes to 1 files
686 686 no changes made to subrepo s since last push to $TESTTMP/t/s
687 687 no changes made to subrepo t since last push to $TESTTMP/t/t
688 688 searching for changes
689 689 no changes found
690 690 [1]
691 691
692 692 a subrepo store may be clean versus one repo but not versus another
693 693
694 694 $ hg push
695 695 pushing to $TESTTMP/t
696 696 no changes made to subrepo s/ss since last push to $TESTTMP/t/s/ss
697 697 no changes made to subrepo s since last push to $TESTTMP/t/s
698 698 no changes made to subrepo t since last push to $TESTTMP/t/t
699 699 searching for changes
700 700 no changes found
701 701 [1]
702 702 $ hg push ../tcc
703 703 pushing to ../tcc
704 704 pushing subrepo s/ss to ../tcc/s/ss
705 705 searching for changes
706 706 adding changesets
707 707 adding manifests
708 708 adding file changes
709 709 added 1 changesets with 1 changes to 1 files
710 710 no changes made to subrepo s since last push to ../tcc/s
711 711 no changes made to subrepo t since last push to ../tcc/t
712 712 searching for changes
713 713 no changes found
714 714 [1]
715 715
716 716 update
717 717
718 718 $ cd ../t
719 719 $ hg up -C # discard our earlier merge
720 720 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
721 721 updated to "c373c8102e68: 12"
722 722 2 other heads for branch "default"
723 723 $ echo blah > t/t
724 724 $ hg ci -m13
725 725 committing subrepository t
726 726
727 727 backout calls revert internally with minimal opts, which should not raise
728 728 KeyError
729 729
730 730 $ hg backout ".^" --no-commit
731 731 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
732 732 changeset c373c8102e68 backed out, don't forget to commit.
733 733
734 734 $ hg up -C # discard changes
735 735 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
736 736 updated to "925c17564ef8: 13"
737 737 2 other heads for branch "default"
738 738
739 739 pull
740 740
741 741 $ cd ../tc
742 742 $ hg pull
743 743 pulling from $TESTTMP/t
744 744 searching for changes
745 745 adding changesets
746 746 adding manifests
747 747 adding file changes
748 748 added 1 changesets with 1 changes to 1 files
749 749 new changesets 925c17564ef8
750 750 (run 'hg update' to get a working copy)
751 751
752 752 should pull t
753 753
754 754 $ hg incoming -S -r `hg log -r tip -T "{node|short}"`
755 755 comparing with $TESTTMP/t
756 756 no changes found
757 757 comparing with $TESTTMP/t/s
758 758 searching for changes
759 759 no changes found
760 760 comparing with $TESTTMP/t/s/ss
761 761 searching for changes
762 762 no changes found
763 763 comparing with $TESTTMP/t/t
764 764 searching for changes
765 765 changeset: 5:52c0adc0515a
766 766 tag: tip
767 767 user: test
768 768 date: Thu Jan 01 00:00:00 1970 +0000
769 769 summary: 13
770 770
771 771
772 772 $ hg up
773 773 pulling subrepo t from $TESTTMP/t/t
774 774 searching for changes
775 775 adding changesets
776 776 adding manifests
777 777 adding file changes
778 778 added 1 changesets with 1 changes to 1 files
779 779 new changesets 52c0adc0515a
780 780 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
781 781 updated to "925c17564ef8: 13"
782 782 2 other heads for branch "default"
783 783 $ cat t/t
784 784 blah
785 785
786 786 bogus subrepo path aborts
787 787
788 788 $ echo 'bogus=[boguspath' >> .hgsub
789 789 $ hg ci -m 'bogus subrepo path'
790 790 abort: missing ] in subrepository source
791 791 [255]
792 792
793 793 Issue1986: merge aborts when trying to merge a subrepo that
794 794 shouldn't need merging
795 795
796 796 # subrepo layout
797 797 #
798 798 # o 5 br
799 799 # /|
800 800 # o | 4 default
801 801 # | |
802 802 # | o 3 br
803 803 # |/|
804 804 # o | 2 default
805 805 # | |
806 806 # | o 1 br
807 807 # |/
808 808 # o 0 default
809 809
810 810 $ cd ..
811 811 $ rm -rf sub
812 812 $ hg init main
813 813 $ cd main
814 814 $ hg init s
815 815 $ cd s
816 816 $ echo a > a
817 817 $ hg ci -Am1
818 818 adding a
819 819 $ hg branch br
820 820 marked working directory as branch br
821 821 (branches are permanent and global, did you want a bookmark?)
822 822 $ echo a >> a
823 823 $ hg ci -m1
824 824 $ hg up default
825 825 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
826 826 $ echo b > b
827 827 $ hg ci -Am1
828 828 adding b
829 829 $ hg up br
830 830 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
831 831 $ hg merge tip
832 832 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
833 833 (branch merge, don't forget to commit)
834 834 $ hg ci -m1
835 835 $ hg up 2
836 836 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
837 837 $ echo c > c
838 838 $ hg ci -Am1
839 839 adding c
840 840 $ hg up 3
841 841 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
842 842 $ hg merge 4
843 843 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
844 844 (branch merge, don't forget to commit)
845 845 $ hg ci -m1
846 846
847 847 # main repo layout:
848 848 #
849 849 # * <-- try to merge default into br again
850 850 # .`|
851 851 # . o 5 br --> substate = 5
852 852 # . |
853 853 # o | 4 default --> substate = 4
854 854 # | |
855 855 # | o 3 br --> substate = 2
856 856 # |/|
857 857 # o | 2 default --> substate = 2
858 858 # | |
859 859 # | o 1 br --> substate = 3
860 860 # |/
861 861 # o 0 default --> substate = 2
862 862
863 863 $ cd ..
864 864 $ echo 's = s' > .hgsub
865 865 $ hg -R s up 2
866 866 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
867 867 $ hg ci -Am1
868 868 adding .hgsub
869 869 $ hg branch br
870 870 marked working directory as branch br
871 871 (branches are permanent and global, did you want a bookmark?)
872 872 $ echo b > b
873 873 $ hg -R s up 3
874 874 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
875 875 $ hg ci -Am1
876 876 adding b
877 877 $ hg up default
878 878 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
879 879 $ echo c > c
880 880 $ hg ci -Am1
881 881 adding c
882 882 $ hg up 1
883 883 2 files updated, 0 files merged, 1 files removed, 0 files unresolved
884 884 $ hg merge 2
885 885 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
886 886 (branch merge, don't forget to commit)
887 887 $ hg ci -m1
888 888 $ hg up 2
889 889 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
890 890 $ hg -R s up 4
891 891 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
892 892 $ echo d > d
893 893 $ hg ci -Am1
894 894 adding d
895 895 $ hg up 3
896 896 2 files updated, 0 files merged, 1 files removed, 0 files unresolved
897 897 $ hg -R s up 5
898 898 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
899 899 $ echo e > e
900 900 $ hg ci -Am1
901 901 adding e
902 902
903 903 $ hg up 5
904 904 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
905 905 $ hg merge 4 # try to merge default into br again
906 906 subrepository s diverged (local revision: f8f13b33206e, remote revision: a3f9062a4f88)
907 907 (M)erge, keep (l)ocal [working copy] or keep (r)emote [merge rev]? m
908 908 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
909 909 (branch merge, don't forget to commit)
910 910 $ cd ..
911 911
912 912 test subrepo delete from .hgsubstate
913 913
914 914 $ hg init testdelete
915 915 $ mkdir testdelete/nested testdelete/nested2
916 916 $ hg init testdelete/nested
917 917 $ hg init testdelete/nested2
918 918 $ echo test > testdelete/nested/foo
919 919 $ echo test > testdelete/nested2/foo
920 920 $ hg -R testdelete/nested add
921 921 adding testdelete/nested/foo
922 922 $ hg -R testdelete/nested2 add
923 923 adding testdelete/nested2/foo
924 924 $ hg -R testdelete/nested ci -m test
925 925 $ hg -R testdelete/nested2 ci -m test
926 926 $ echo nested = nested > testdelete/.hgsub
927 927 $ echo nested2 = nested2 >> testdelete/.hgsub
928 928 $ hg -R testdelete add
929 929 adding testdelete/.hgsub
930 930 $ hg -R testdelete ci -m "nested 1 & 2 added"
931 931 $ echo nested = nested > testdelete/.hgsub
932 932 $ hg -R testdelete ci -m "nested 2 deleted"
933 933 $ cat testdelete/.hgsubstate
934 934 bdf5c9a3103743d900b12ae0db3ffdcfd7b0d878 nested
935 935 $ hg -R testdelete remove testdelete/.hgsub
936 936 $ hg -R testdelete ci -m ".hgsub deleted"
937 937 $ cat testdelete/.hgsubstate
938 938 bdf5c9a3103743d900b12ae0db3ffdcfd7b0d878 nested
939 939
940 940 test repository cloning
941 941
942 942 $ mkdir mercurial mercurial2
943 943 $ hg init nested_absolute
944 944 $ echo test > nested_absolute/foo
945 945 $ hg -R nested_absolute add
946 946 adding nested_absolute/foo
947 947 $ hg -R nested_absolute ci -mtest
948 948 $ cd mercurial
949 949 $ hg init nested_relative
950 950 $ echo test2 > nested_relative/foo2
951 951 $ hg -R nested_relative add
952 952 adding nested_relative/foo2
953 953 $ hg -R nested_relative ci -mtest2
954 954 $ hg init main
955 955 $ echo "nested_relative = ../nested_relative" > main/.hgsub
956 956 $ echo "nested_absolute = `pwd`/nested_absolute" >> main/.hgsub
957 957 $ hg -R main add
958 958 adding main/.hgsub
959 959 $ hg -R main ci -m "add subrepos"
960 960 $ cd ..
961 961 $ hg clone mercurial/main mercurial2/main
962 962 updating to branch default
963 963 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
964 964 $ cat mercurial2/main/nested_absolute/.hg/hgrc \
965 965 > mercurial2/main/nested_relative/.hg/hgrc
966 966 [paths]
967 967 default = $TESTTMP/mercurial/nested_absolute
968 968 [paths]
969 969 default = $TESTTMP/mercurial/nested_relative
970 970 $ rm -rf mercurial mercurial2
971 971
972 972 Issue1977: multirepo push should fail if subrepo push fails
973 973
974 974 $ hg init repo
975 975 $ hg init repo/s
976 976 $ echo a > repo/s/a
977 977 $ hg -R repo/s ci -Am0
978 978 adding a
979 979 $ echo s = s > repo/.hgsub
980 980 $ hg -R repo ci -Am1
981 981 adding .hgsub
982 982 $ hg clone repo repo2
983 983 updating to branch default
984 984 cloning subrepo s from $TESTTMP/repo/s
985 985 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
986 986 $ hg -q -R repo2 pull -u
987 987 $ echo 1 > repo2/s/a
988 988 $ hg -R repo2/s ci -m2
989 989 $ hg -q -R repo2/s push
990 990 $ hg -R repo2/s up -C 0
991 991 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
992 992 $ echo 2 > repo2/s/b
993 993 $ hg -R repo2/s ci -m3 -A
994 994 adding b
995 995 created new head
996 996 $ hg -R repo2 ci -m3
997 997 $ hg -q -R repo2 push
998 998 abort: push creates new remote head cc505f09a8b2! (in subrepository "s")
999 999 (merge or see 'hg help push' for details about pushing new heads)
1000 1000 [255]
1001 1001 $ hg -R repo update
1002 1002 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
1003 1003
1004 1004 test if untracked file is not overwritten
1005 1005
1006 1006 (this also tests that updated .hgsubstate is treated as "modified",
1007 1007 when 'merge.update()' is aborted before 'merge.recordupdates()', even
1008 1008 if none of mode, size and timestamp of it isn't changed on the
1009 1009 filesystem (see also issue4583))
1010 1010
1011 1011 $ echo issue3276_ok > repo/s/b
1012 1012 $ hg -R repo2 push -f -q
1013 1013 $ touch -t 200001010000 repo/.hgsubstate
1014 1014
1015 1015 $ cat >> repo/.hg/hgrc <<EOF
1016 1016 > [fakedirstatewritetime]
1017 1017 > # emulate invoking dirstate.write() via repo.status()
1018 1018 > # at 2000-01-01 00:00
1019 1019 > fakenow = 200001010000
1020 1020 >
1021 1021 > [extensions]
1022 1022 > fakedirstatewritetime = $TESTDIR/fakedirstatewritetime.py
1023 1023 > EOF
1024 1024 $ hg -R repo update
1025 1025 b: untracked file differs
1026 1026 abort: untracked files in working directory differ from files in requested revision (in subrepository "s")
1027 1027 [255]
1028 1028 $ cat >> repo/.hg/hgrc <<EOF
1029 1029 > [extensions]
1030 1030 > fakedirstatewritetime = !
1031 1031 > EOF
1032 1032
1033 1033 $ cat repo/s/b
1034 1034 issue3276_ok
1035 1035 $ rm repo/s/b
1036 1036 $ touch -t 200001010000 repo/.hgsubstate
1037 1037 $ hg -R repo revert --all
1038 1038 reverting repo/.hgsubstate
1039 1039 reverting subrepo s
1040 1040 $ hg -R repo update
1041 1041 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1042 1042 $ cat repo/s/b
1043 1043 2
1044 1044 $ rm -rf repo2 repo
1045 1045
1046 1046
1047 1047 Issue1852 subrepos with relative paths always push/pull relative to default
1048 1048
1049 1049 Prepare a repo with subrepo
1050 1050
1051 1051 $ hg init issue1852a
1052 1052 $ cd issue1852a
1053 1053 $ hg init sub/repo
1054 1054 $ echo test > sub/repo/foo
1055 1055 $ hg -R sub/repo add sub/repo/foo
1056 1056 $ echo sub/repo = sub/repo > .hgsub
1057 1057 $ hg add .hgsub
1058 1058 $ hg ci -mtest
1059 1059 committing subrepository sub/repo
1060 1060 $ echo test >> sub/repo/foo
1061 1061 $ hg ci -mtest
1062 1062 committing subrepository sub/repo
1063 1063 $ hg cat sub/repo/foo
1064 1064 test
1065 1065 test
1066 1066 $ hg cat sub/repo/foo -Tjson | sed 's|\\\\|/|g'
1067 1067 [
1068 1068 {
1069 1069 "abspath": "foo",
1070 1070 "data": "test\ntest\n",
1071 1071 "path": "sub/repo/foo"
1072 1072 }
1073 1073 ]
1074 1074
1075 1075 non-exact match:
1076 1076
1077 1077 $ hg cat -T '{path}\n' 'glob:**'
1078 1078 .hgsub
1079 1079 .hgsubstate
1080 1080 sub/repo/foo
1081 1081 $ hg cat -T '{path}\n' 're:^sub'
1082 1082 sub/repo/foo
1083 1083
1084 1084 missing subrepos in working directory:
1085 1085
1086 1086 $ mkdir -p tmp/sub/repo
1087 1087 $ hg cat -r 0 --output tmp/%p_p sub/repo/foo
1088 1088 $ cat tmp/sub/repo/foo_p
1089 1089 test
1090 1090 $ mv sub/repo sub_
1091 1091 $ hg cat sub/repo/baz
1092 1092 skipping missing subrepository: sub/repo
1093 1093 [1]
1094 1094 $ rm -rf sub/repo
1095 1095 $ mv sub_ sub/repo
1096 1096 $ cd ..
1097 1097
1098 1098 Create repo without default path, pull top repo, and see what happens on update
1099 1099
1100 1100 $ hg init issue1852b
1101 1101 $ hg -R issue1852b pull issue1852a
1102 1102 pulling from issue1852a
1103 1103 requesting all changes
1104 1104 adding changesets
1105 1105 adding manifests
1106 1106 adding file changes
1107 1107 added 2 changesets with 3 changes to 2 files
1108 1108 new changesets 19487b456929:be5eb94e7215
1109 1109 (run 'hg update' to get a working copy)
1110 1110 $ hg -R issue1852b update
1111 1111 abort: default path for subrepository not found (in subrepository "sub/repo")
1112 1112 [255]
1113 1113
1114 1114 Ensure a full traceback, not just the SubrepoAbort part
1115 1115
1116 1116 $ hg -R issue1852b update --traceback 2>&1 | grep 'raise error\.Abort'
1117 1117 raise error.Abort(_("default path for subrepository not found"))
1118 1118
1119 1119 Pull -u now doesn't help
1120 1120
1121 1121 $ hg -R issue1852b pull -u issue1852a
1122 1122 pulling from issue1852a
1123 1123 searching for changes
1124 1124 no changes found
1125 1125
1126 1126 Try the same, but with pull -u
1127 1127
1128 1128 $ hg init issue1852c
1129 1129 $ hg -R issue1852c pull -r0 -u issue1852a
1130 1130 pulling from issue1852a
1131 1131 adding changesets
1132 1132 adding manifests
1133 1133 adding file changes
1134 1134 added 1 changesets with 2 changes to 2 files
1135 1135 new changesets 19487b456929
1136 1136 cloning subrepo sub/repo from issue1852a/sub/repo
1137 1137 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
1138 1138
1139 1139 Try to push from the other side
1140 1140
1141 1141 $ hg -R issue1852a push `pwd`/issue1852c
1142 1142 pushing to $TESTTMP/issue1852c
1143 1143 pushing subrepo sub/repo to $TESTTMP/issue1852c/sub/repo
1144 1144 searching for changes
1145 1145 no changes found
1146 1146 searching for changes
1147 1147 adding changesets
1148 1148 adding manifests
1149 1149 adding file changes
1150 1150 added 1 changesets with 1 changes to 1 files
1151 1151
1152 1152 Incoming and outgoing should not use the default path:
1153 1153
1154 1154 $ hg clone -q issue1852a issue1852d
1155 1155 $ hg -R issue1852d outgoing --subrepos issue1852c
1156 1156 comparing with issue1852c
1157 1157 searching for changes
1158 1158 no changes found
1159 1159 comparing with issue1852c/sub/repo
1160 1160 searching for changes
1161 1161 no changes found
1162 1162 [1]
1163 1163 $ hg -R issue1852d incoming --subrepos issue1852c
1164 1164 comparing with issue1852c
1165 1165 searching for changes
1166 1166 no changes found
1167 1167 comparing with issue1852c/sub/repo
1168 1168 searching for changes
1169 1169 no changes found
1170 1170 [1]
1171 1171
1172 1172 Check that merge of a new subrepo doesn't write the uncommitted state to
1173 1173 .hgsubstate (issue4622)
1174 1174
1175 1175 $ hg init issue1852a/addedsub
1176 1176 $ echo zzz > issue1852a/addedsub/zz.txt
1177 1177 $ hg -R issue1852a/addedsub ci -Aqm "initial ZZ"
1178 1178
1179 1179 $ hg clone issue1852a/addedsub issue1852d/addedsub
1180 1180 updating to branch default
1181 1181 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1182 1182
1183 1183 $ echo def > issue1852a/sub/repo/foo
1184 1184 $ hg -R issue1852a ci -SAm 'tweaked subrepo'
1185 1185 adding tmp/sub/repo/foo_p
1186 1186 committing subrepository sub/repo
1187 1187
1188 1188 $ echo 'addedsub = addedsub' >> issue1852d/.hgsub
1189 1189 $ echo xyz > issue1852d/sub/repo/foo
1190 1190 $ hg -R issue1852d pull -u
1191 1191 pulling from $TESTTMP/issue1852a
1192 1192 searching for changes
1193 1193 adding changesets
1194 1194 adding manifests
1195 1195 adding file changes
1196 1196 added 1 changesets with 2 changes to 2 files
1197 1197 new changesets c82b79fdcc5b
1198 1198 subrepository sub/repo diverged (local revision: f42d5c7504a8, remote revision: 46cd4aac504c)
1199 1199 (M)erge, keep (l)ocal [working copy] or keep (r)emote [destination]? m
1200 1200 pulling subrepo sub/repo from $TESTTMP/issue1852a/sub/repo
1201 1201 searching for changes
1202 1202 adding changesets
1203 1203 adding manifests
1204 1204 adding file changes
1205 1205 added 1 changesets with 1 changes to 1 files
1206 1206 new changesets 46cd4aac504c
1207 1207 subrepository sources for sub/repo differ
1208 1208 use (l)ocal source (f42d5c7504a8) or (r)emote source (46cd4aac504c)? l
1209 1209 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1210 1210 $ cat issue1852d/.hgsubstate
1211 1211 f42d5c7504a811dda50f5cf3e5e16c3330b87172 sub/repo
1212 1212
1213 1213 Check status of files when none of them belong to the first
1214 1214 subrepository:
1215 1215
1216 1216 $ hg init subrepo-status
1217 1217 $ cd subrepo-status
1218 1218 $ hg init subrepo-1
1219 1219 $ hg init subrepo-2
1220 1220 $ cd subrepo-2
1221 1221 $ touch file
1222 1222 $ hg add file
1223 1223 $ cd ..
1224 1224 $ echo subrepo-1 = subrepo-1 > .hgsub
1225 1225 $ echo subrepo-2 = subrepo-2 >> .hgsub
1226 1226 $ hg add .hgsub
1227 1227 $ hg ci -m 'Added subrepos'
1228 1228 committing subrepository subrepo-2
1229 1229 $ hg st subrepo-2/file
1230 1230
1231 1231 Check that share works with subrepo
1232 1232 $ hg --config extensions.share= share . ../shared
1233 1233 updating working directory
1234 1234 sharing subrepo subrepo-1 from $TESTTMP/subrepo-status/subrepo-1
1235 1235 sharing subrepo subrepo-2 from $TESTTMP/subrepo-status/subrepo-2
1236 1236 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
1237 1237 $ find ../shared/* | sort
1238 1238 ../shared/subrepo-1
1239 1239 ../shared/subrepo-1/.hg
1240 1240 ../shared/subrepo-1/.hg/cache
1241 1241 ../shared/subrepo-1/.hg/cache/storehash
1242 1242 ../shared/subrepo-1/.hg/cache/storehash/* (glob)
1243 1243 ../shared/subrepo-1/.hg/hgrc
1244 1244 ../shared/subrepo-1/.hg/requires
1245 1245 ../shared/subrepo-1/.hg/sharedpath
1246 1246 ../shared/subrepo-2
1247 1247 ../shared/subrepo-2/.hg
1248 1248 ../shared/subrepo-2/.hg/branch
1249 1249 ../shared/subrepo-2/.hg/cache
1250 1250 ../shared/subrepo-2/.hg/cache/checkisexec (execbit !)
1251 1251 ../shared/subrepo-2/.hg/cache/checklink (symlink !)
1252 1252 ../shared/subrepo-2/.hg/cache/checklink-target (symlink !)
1253 1253 ../shared/subrepo-2/.hg/cache/storehash
1254 1254 ../shared/subrepo-2/.hg/cache/storehash/* (glob)
1255 1255 ../shared/subrepo-2/.hg/dirstate
1256 1256 ../shared/subrepo-2/.hg/hgrc
1257 1257 ../shared/subrepo-2/.hg/requires
1258 1258 ../shared/subrepo-2/.hg/sharedpath
1259 1259 ../shared/subrepo-2/file
1260 1260 $ hg -R ../shared in
1261 1261 abort: repository default not found!
1262 1262 [255]
1263 1263 $ hg -R ../shared/subrepo-2 showconfig paths
1264 1264 paths.default=$TESTTMP/subrepo-status/subrepo-2
1265 1265 $ hg -R ../shared/subrepo-1 sum --remote
1266 1266 parent: -1:000000000000 tip (empty repository)
1267 1267 branch: default
1268 1268 commit: (clean)
1269 1269 update: (current)
1270 1270 remote: (synced)
1271 1271
1272 1272 Check hg update --clean
1273 1273 $ cd $TESTTMP/t
1274 1274 $ rm -r t/t.orig
1275 1275 $ hg status -S --all
1276 1276 C .hgsub
1277 1277 C .hgsubstate
1278 1278 C a
1279 1279 C s/.hgsub
1280 1280 C s/.hgsubstate
1281 1281 C s/a
1282 1282 C s/ss/a
1283 1283 C t/t
1284 1284 $ echo c1 > s/a
1285 1285 $ cd s
1286 1286 $ echo c1 > b
1287 1287 $ echo c1 > c
1288 1288 $ hg add b
1289 1289 $ cd ..
1290 1290 $ hg status -S
1291 1291 M s/a
1292 1292 A s/b
1293 1293 ? s/c
1294 1294 $ hg update -C
1295 1295 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1296 1296 updated to "925c17564ef8: 13"
1297 1297 2 other heads for branch "default"
1298 1298 $ hg status -S
1299 1299 ? s/b
1300 1300 ? s/c
1301 1301
1302 1302 Sticky subrepositories, no changes
1303 1303 $ cd $TESTTMP/t
1304 1304 $ hg id
1305 1305 925c17564ef8 tip
1306 1306 $ hg -R s id
1307 1307 12a213df6fa9 tip
1308 1308 $ hg -R t id
1309 1309 52c0adc0515a tip
1310 1310 $ hg update 11
1311 1311 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1312 1312 $ hg id
1313 1313 365661e5936a
1314 1314 $ hg -R s id
1315 1315 fc627a69481f
1316 1316 $ hg -R t id
1317 1317 e95bcfa18a35
1318 1318
1319 1319 Sticky subrepositories, file changes
1320 1320 $ touch s/f1
1321 1321 $ touch t/f1
1322 1322 $ hg add -S s/f1
1323 1323 $ hg add -S t/f1
1324 1324 $ hg id
1325 1325 365661e5936a+
1326 1326 $ hg -R s id
1327 1327 fc627a69481f+
1328 1328 $ hg -R t id
1329 1329 e95bcfa18a35+
1330 1330 $ hg update tip
1331 1331 subrepository s diverged (local revision: fc627a69481f, remote revision: 12a213df6fa9)
1332 1332 (M)erge, keep (l)ocal [working copy] or keep (r)emote [destination]? m
1333 1333 subrepository sources for s differ
1334 1334 use (l)ocal source (fc627a69481f) or (r)emote source (12a213df6fa9)? l
1335 1335 subrepository t diverged (local revision: e95bcfa18a35, remote revision: 52c0adc0515a)
1336 1336 (M)erge, keep (l)ocal [working copy] or keep (r)emote [destination]? m
1337 1337 subrepository sources for t differ
1338 1338 use (l)ocal source (e95bcfa18a35) or (r)emote source (52c0adc0515a)? l
1339 1339 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
1340 1340 $ hg id
1341 1341 925c17564ef8+ tip
1342 1342 $ hg -R s id
1343 1343 fc627a69481f+
1344 1344 $ hg -R t id
1345 1345 e95bcfa18a35+
1346 1346 $ hg update --clean tip
1347 1347 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1348 1348
1349 1349 Sticky subrepository, revision updates
1350 1350 $ hg id
1351 1351 925c17564ef8 tip
1352 1352 $ hg -R s id
1353 1353 12a213df6fa9 tip
1354 1354 $ hg -R t id
1355 1355 52c0adc0515a tip
1356 1356 $ cd s
1357 1357 $ hg update -r -2
1358 1358 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1359 1359 $ cd ../t
1360 1360 $ hg update -r 2
1361 1361 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1362 1362 $ cd ..
1363 1363 $ hg update 10
1364 1364 subrepository s diverged (local revision: 12a213df6fa9, remote revision: fc627a69481f)
1365 1365 (M)erge, keep (l)ocal [working copy] or keep (r)emote [destination]? m
1366 1366 subrepository t diverged (local revision: 52c0adc0515a, remote revision: 20a0db6fbf6c)
1367 1367 (M)erge, keep (l)ocal [working copy] or keep (r)emote [destination]? m
1368 1368 subrepository sources for t differ (in checked out version)
1369 1369 use (l)ocal source (7af322bc1198) or (r)emote source (20a0db6fbf6c)? l
1370 1370 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
1371 1371 $ hg id
1372 1372 e45c8b14af55+
1373 1373 $ hg -R s id
1374 1374 02dcf1d70411
1375 1375 $ hg -R t id
1376 1376 7af322bc1198
1377 1377
1378 1378 Sticky subrepository, file changes and revision updates
1379 1379 $ touch s/f1
1380 1380 $ touch t/f1
1381 1381 $ hg add -S s/f1
1382 1382 $ hg add -S t/f1
1383 1383 $ hg id
1384 1384 e45c8b14af55+
1385 1385 $ hg -R s id
1386 1386 02dcf1d70411+
1387 1387 $ hg -R t id
1388 1388 7af322bc1198+
1389 1389 $ hg update tip
1390 1390 subrepository s diverged (local revision: 12a213df6fa9, remote revision: 12a213df6fa9)
1391 1391 (M)erge, keep (l)ocal [working copy] or keep (r)emote [destination]? m
1392 1392 subrepository sources for s differ
1393 1393 use (l)ocal source (02dcf1d70411) or (r)emote source (12a213df6fa9)? l
1394 1394 subrepository t diverged (local revision: 52c0adc0515a, remote revision: 52c0adc0515a)
1395 1395 (M)erge, keep (l)ocal [working copy] or keep (r)emote [destination]? m
1396 1396 subrepository sources for t differ
1397 1397 use (l)ocal source (7af322bc1198) or (r)emote source (52c0adc0515a)? l
1398 1398 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
1399 1399 $ hg id
1400 1400 925c17564ef8+ tip
1401 1401 $ hg -R s id
1402 1402 02dcf1d70411+
1403 1403 $ hg -R t id
1404 1404 7af322bc1198+
1405 1405
1406 1406 Sticky repository, update --clean
1407 1407 $ hg update --clean tip
1408 1408 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1409 1409 $ hg id
1410 1410 925c17564ef8 tip
1411 1411 $ hg -R s id
1412 1412 12a213df6fa9 tip
1413 1413 $ hg -R t id
1414 1414 52c0adc0515a tip
1415 1415
1416 1416 Test subrepo already at intended revision:
1417 1417 $ cd s
1418 1418 $ hg update fc627a69481f
1419 1419 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1420 1420 $ cd ..
1421 1421 $ hg update 11
1422 1422 subrepository s diverged (local revision: 12a213df6fa9, remote revision: fc627a69481f)
1423 1423 (M)erge, keep (l)ocal [working copy] or keep (r)emote [destination]? m
1424 1424 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
1425 1425 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
1426 1426 $ hg id -n
1427 1427 11+
1428 1428 $ hg -R s id
1429 1429 fc627a69481f
1430 1430 $ hg -R t id
1431 1431 e95bcfa18a35
1432 1432
1433 1433 Test that removing .hgsubstate doesn't break anything:
1434 1434
1435 1435 $ hg rm -f .hgsubstate
1436 1436 $ hg ci -mrm
1437 1437 nothing changed
1438 1438 [1]
1439 1439 $ hg log -vr tip
1440 1440 changeset: 13:925c17564ef8
1441 1441 tag: tip
1442 1442 user: test
1443 1443 date: Thu Jan 01 00:00:00 1970 +0000
1444 1444 files: .hgsubstate
1445 1445 description:
1446 1446 13
1447 1447
1448 1448
1449 1449
1450 1450 Test that removing .hgsub removes .hgsubstate:
1451 1451
1452 1452 $ hg rm .hgsub
1453 1453 $ hg ci -mrm2
1454 1454 created new head
1455 1455 $ hg log -vr tip
1456 1456 changeset: 14:2400bccd50af
1457 1457 tag: tip
1458 1458 parent: 11:365661e5936a
1459 1459 user: test
1460 1460 date: Thu Jan 01 00:00:00 1970 +0000
1461 1461 files: .hgsub .hgsubstate
1462 1462 description:
1463 1463 rm2
1464 1464
1465 1465
1466 1466 Test issue3153: diff -S with deleted subrepos
1467 1467
1468 1468 $ hg diff --nodates -S -c .
1469 1469 diff -r 365661e5936a -r 2400bccd50af .hgsub
1470 1470 --- a/.hgsub
1471 1471 +++ /dev/null
1472 1472 @@ -1,2 +0,0 @@
1473 1473 -s = s
1474 1474 -t = t
1475 1475 diff -r 365661e5936a -r 2400bccd50af .hgsubstate
1476 1476 --- a/.hgsubstate
1477 1477 +++ /dev/null
1478 1478 @@ -1,2 +0,0 @@
1479 1479 -fc627a69481fcbe5f1135069e8a3881c023e4cf5 s
1480 1480 -e95bcfa18a358dc4936da981ebf4147b4cad1362 t
1481 1481
1482 1482 Test behavior of add for explicit path in subrepo:
1483 1483 $ cd ..
1484 1484 $ hg init explicit
1485 1485 $ cd explicit
1486 1486 $ echo s = s > .hgsub
1487 1487 $ hg add .hgsub
1488 1488 $ hg init s
1489 1489 $ hg ci -m0
1490 1490 Adding with an explicit path in a subrepo adds the file
1491 1491 $ echo c1 > f1
1492 1492 $ echo c2 > s/f2
1493 1493 $ hg st -S
1494 1494 ? f1
1495 1495 ? s/f2
1496 1496 $ hg add s/f2
1497 1497 $ hg st -S
1498 1498 A s/f2
1499 1499 ? f1
1500 1500 $ hg ci -R s -m0
1501 1501 $ hg ci -Am1
1502 1502 adding f1
1503 1503 Adding with an explicit path in a subrepo with -S has the same behavior
1504 1504 $ echo c3 > f3
1505 1505 $ echo c4 > s/f4
1506 1506 $ hg st -S
1507 1507 ? f3
1508 1508 ? s/f4
1509 1509 $ hg add -S s/f4
1510 1510 $ hg st -S
1511 1511 A s/f4
1512 1512 ? f3
1513 1513 $ hg ci -R s -m1
1514 1514 $ hg ci -Ama2
1515 1515 adding f3
1516 1516 Adding without a path or pattern silently ignores subrepos
1517 1517 $ echo c5 > f5
1518 1518 $ echo c6 > s/f6
1519 1519 $ echo c7 > s/f7
1520 1520 $ hg st -S
1521 1521 ? f5
1522 1522 ? s/f6
1523 1523 ? s/f7
1524 1524 $ hg add
1525 1525 adding f5
1526 1526 $ hg st -S
1527 1527 A f5
1528 1528 ? s/f6
1529 1529 ? s/f7
1530 1530 $ hg ci -R s -Am2
1531 1531 adding f6
1532 1532 adding f7
1533 1533 $ hg ci -m3
1534 1534 Adding without a path or pattern with -S also adds files in subrepos
1535 1535 $ echo c8 > f8
1536 1536 $ echo c9 > s/f9
1537 1537 $ echo c10 > s/f10
1538 1538 $ hg st -S
1539 1539 ? f8
1540 1540 ? s/f10
1541 1541 ? s/f9
1542 1542 $ hg add -S
1543 1543 adding f8
1544 1544 adding s/f10
1545 1545 adding s/f9
1546 1546 $ hg st -S
1547 1547 A f8
1548 1548 A s/f10
1549 1549 A s/f9
1550 1550 $ hg ci -R s -m3
1551 1551 $ hg ci -m4
1552 1552 Adding with a pattern silently ignores subrepos
1553 1553 $ echo c11 > fm11
1554 1554 $ echo c12 > fn12
1555 1555 $ echo c13 > s/fm13
1556 1556 $ echo c14 > s/fn14
1557 1557 $ hg st -S
1558 1558 ? fm11
1559 1559 ? fn12
1560 1560 ? s/fm13
1561 1561 ? s/fn14
1562 1562 $ hg add 'glob:**fm*'
1563 1563 adding fm11
1564 1564 $ hg st -S
1565 1565 A fm11
1566 1566 ? fn12
1567 1567 ? s/fm13
1568 1568 ? s/fn14
1569 1569 $ hg ci -R s -Am4
1570 1570 adding fm13
1571 1571 adding fn14
1572 1572 $ hg ci -Am5
1573 1573 adding fn12
1574 1574 Adding with a pattern with -S also adds matches in subrepos
1575 1575 $ echo c15 > fm15
1576 1576 $ echo c16 > fn16
1577 1577 $ echo c17 > s/fm17
1578 1578 $ echo c18 > s/fn18
1579 1579 $ hg st -S
1580 1580 ? fm15
1581 1581 ? fn16
1582 1582 ? s/fm17
1583 1583 ? s/fn18
1584 1584 $ hg add -S 'glob:**fm*'
1585 1585 adding fm15
1586 1586 adding s/fm17
1587 1587 $ hg st -S
1588 1588 A fm15
1589 1589 A s/fm17
1590 1590 ? fn16
1591 1591 ? s/fn18
1592 1592 $ hg ci -R s -Am5
1593 1593 adding fn18
1594 1594 $ hg ci -Am6
1595 1595 adding fn16
1596 1596
1597 1597 Test behavior of forget for explicit path in subrepo:
1598 1598 Forgetting an explicit path in a subrepo untracks the file
1599 1599 $ echo c19 > s/f19
1600 1600 $ hg add s/f19
1601 1601 $ hg st -S
1602 1602 A s/f19
1603 1603 $ hg forget s/f19
1604 1604 $ hg st -S
1605 1605 ? s/f19
1606 1606 $ rm s/f19
1607 1607 $ cd ..
1608 1608
1609 1609 Courtesy phases synchronisation to publishing server does not block the push
1610 1610 (issue3781)
1611 1611
1612 1612 $ cp -R main issue3781
1613 1613 $ cp -R main issue3781-dest
1614 1614 $ cd issue3781-dest/s
1615 1615 $ hg phase tip # show we have draft changeset
1616 1616 5: draft
1617 1617 $ chmod a-w .hg/store/phaseroots # prevent phase push
1618 1618 $ cd ../../issue3781
1619 1619 $ cat >> .hg/hgrc << EOF
1620 1620 > [paths]
1621 1621 > default=../issue3781-dest/
1622 1622 > EOF
1623 1623 $ hg push --config devel.legacy.exchange=bundle1
1624 1624 pushing to $TESTTMP/issue3781-dest
1625 1625 pushing subrepo s to $TESTTMP/issue3781-dest/s
1626 1626 searching for changes
1627 1627 no changes found
1628 1628 searching for changes
1629 1629 no changes found
1630 1630 [1]
1631 1631 # clean the push cache
1632 1632 $ rm s/.hg/cache/storehash/*
1633 1633 $ hg push # bundle2+
1634 1634 pushing to $TESTTMP/issue3781-dest
1635 1635 pushing subrepo s to $TESTTMP/issue3781-dest/s
1636 1636 searching for changes
1637 1637 no changes found
1638 1638 searching for changes
1639 1639 no changes found
1640 1640 [1]
1641 1641 $ cd ..
1642 1642
1643 1643 Test phase choice for newly created commit with "phases.subrepochecks"
1644 1644 configuration
1645 1645
1646 1646 $ cd t
1647 1647 $ hg update -q -r 12
1648 1648
1649 1649 $ cat >> s/ss/.hg/hgrc <<EOF
1650 1650 > [phases]
1651 1651 > new-commit = secret
1652 1652 > EOF
1653 1653 $ cat >> s/.hg/hgrc <<EOF
1654 1654 > [phases]
1655 1655 > new-commit = draft
1656 1656 > EOF
1657 1657 $ echo phasecheck1 >> s/ss/a
1658 1658 $ hg -R s commit -S --config phases.checksubrepos=abort -m phasecheck1
1659 1659 committing subrepository ss
1660 1660 transaction abort!
1661 1661 rollback completed
1662 1662 abort: can't commit in draft phase conflicting secret from subrepository ss
1663 1663 [255]
1664 1664 $ echo phasecheck2 >> s/ss/a
1665 1665 $ hg -R s commit -S --config phases.checksubrepos=ignore -m phasecheck2
1666 1666 committing subrepository ss
1667 1667 $ hg -R s/ss phase tip
1668 1668 3: secret
1669 1669 $ hg -R s phase tip
1670 1670 6: draft
1671 1671 $ echo phasecheck3 >> s/ss/a
1672 1672 $ hg -R s commit -S -m phasecheck3
1673 1673 committing subrepository ss
1674 1674 warning: changes are committed in secret phase from subrepository ss
1675 1675 $ hg -R s/ss phase tip
1676 1676 4: secret
1677 1677 $ hg -R s phase tip
1678 1678 7: secret
1679 1679
1680 1680 $ cat >> t/.hg/hgrc <<EOF
1681 1681 > [phases]
1682 1682 > new-commit = draft
1683 1683 > EOF
1684 1684 $ cat >> .hg/hgrc <<EOF
1685 1685 > [phases]
1686 1686 > new-commit = public
1687 1687 > EOF
1688 1688 $ echo phasecheck4 >> s/ss/a
1689 1689 $ echo phasecheck4 >> t/t
1690 1690 $ hg commit -S -m phasecheck4
1691 1691 committing subrepository s
1692 1692 committing subrepository s/ss
1693 1693 warning: changes are committed in secret phase from subrepository ss
1694 1694 committing subrepository t
1695 1695 warning: changes are committed in secret phase from subrepository s
1696 1696 created new head
1697 1697 $ hg -R s/ss phase tip
1698 1698 5: secret
1699 1699 $ hg -R s phase tip
1700 1700 8: secret
1701 1701 $ hg -R t phase tip
1702 1702 6: draft
1703 1703 $ hg phase tip
1704 1704 15: secret
1705 1705
1706 1706 $ cd ..
1707 1707
1708 1708
1709 1709 Test that commit --secret works on both repo and subrepo (issue4182)
1710 1710
1711 1711 $ cd main
1712 1712 $ echo secret >> b
1713 1713 $ echo secret >> s/b
1714 1714 $ hg commit --secret --subrepo -m "secret"
1715 1715 committing subrepository s
1716 1716 $ hg phase -r .
1717 1717 6: secret
1718 1718 $ cd s
1719 1719 $ hg phase -r .
1720 1720 6: secret
1721 1721 $ cd ../../
1722 1722
1723 1723 Test "subrepos" template keyword
1724 1724
1725 1725 $ cd t
1726 1726 $ hg update -q 15
1727 1727 $ cat > .hgsub <<EOF
1728 1728 > s = s
1729 1729 > EOF
1730 1730 $ hg commit -m "16"
1731 1731 warning: changes are committed in secret phase from subrepository s
1732 1732
1733 1733 (addition of ".hgsub" itself)
1734 1734
1735 1735 $ hg diff --nodates -c 1 .hgsubstate
1736 1736 diff -r f7b1eb17ad24 -r 7cf8cfea66e4 .hgsubstate
1737 1737 --- /dev/null
1738 1738 +++ b/.hgsubstate
1739 1739 @@ -0,0 +1,1 @@
1740 1740 +e4ece1bf43360ddc8f6a96432201a37b7cd27ae4 s
1741 1741 $ hg log -r 1 --template "{p1node|short} {p2node|short}\n{subrepos % '{subrepo}\n'}"
1742 1742 f7b1eb17ad24 000000000000
1743 1743 s
1744 1744
1745 1745 (modification of existing entry)
1746 1746
1747 1747 $ hg diff --nodates -c 2 .hgsubstate
1748 1748 diff -r 7cf8cfea66e4 -r df30734270ae .hgsubstate
1749 1749 --- a/.hgsubstate
1750 1750 +++ b/.hgsubstate
1751 1751 @@ -1,1 +1,1 @@
1752 1752 -e4ece1bf43360ddc8f6a96432201a37b7cd27ae4 s
1753 1753 +dc73e2e6d2675eb2e41e33c205f4bdab4ea5111d s
1754 1754 $ hg log -r 2 --template "{p1node|short} {p2node|short}\n{subrepos % '{subrepo}\n'}"
1755 1755 7cf8cfea66e4 000000000000
1756 1756 s
1757 1757
1758 1758 (addition of entry)
1759 1759
1760 1760 $ hg diff --nodates -c 5 .hgsubstate
1761 1761 diff -r 7cf8cfea66e4 -r 1f14a2e2d3ec .hgsubstate
1762 1762 --- a/.hgsubstate
1763 1763 +++ b/.hgsubstate
1764 1764 @@ -1,1 +1,2 @@
1765 1765 e4ece1bf43360ddc8f6a96432201a37b7cd27ae4 s
1766 1766 +60ca1237c19474e7a3978b0dc1ca4e6f36d51382 t
1767 1767 $ hg log -r 5 --template "{p1node|short} {p2node|short}\n{subrepos % '{subrepo}\n'}"
1768 1768 7cf8cfea66e4 000000000000
1769 1769 t
1770 1770
1771 1771 (removal of existing entry)
1772 1772
1773 1773 $ hg diff --nodates -c 16 .hgsubstate
1774 1774 diff -r 8bec38d2bd0b -r f2f70bc3d3c9 .hgsubstate
1775 1775 --- a/.hgsubstate
1776 1776 +++ b/.hgsubstate
1777 1777 @@ -1,2 +1,1 @@
1778 1778 0731af8ca9423976d3743119d0865097c07bdc1b s
1779 1779 -e202dc79b04c88a636ea8913d9182a1346d9b3dc t
1780 1780 $ hg log -r 16 --template "{p1node|short} {p2node|short}\n{subrepos % '{subrepo}\n'}"
1781 1781 8bec38d2bd0b 000000000000
1782 1782 t
1783 1783
1784 1784 (merging)
1785 1785
1786 1786 $ hg diff --nodates -c 9 .hgsubstate
1787 1787 diff -r f6affe3fbfaa -r f0d2028bf86d .hgsubstate
1788 1788 --- a/.hgsubstate
1789 1789 +++ b/.hgsubstate
1790 1790 @@ -1,1 +1,2 @@
1791 1791 fc627a69481fcbe5f1135069e8a3881c023e4cf5 s
1792 1792 +60ca1237c19474e7a3978b0dc1ca4e6f36d51382 t
1793 1793 $ hg log -r 9 --template "{p1node|short} {p2node|short}\n{subrepos % '{subrepo}\n'}"
1794 1794 f6affe3fbfaa 1f14a2e2d3ec
1795 1795 t
1796 1796
1797 1797 (removal of ".hgsub" itself)
1798 1798
1799 1799 $ hg diff --nodates -c 8 .hgsubstate
1800 1800 diff -r f94576341bcf -r 96615c1dad2d .hgsubstate
1801 1801 --- a/.hgsubstate
1802 1802 +++ /dev/null
1803 1803 @@ -1,2 +0,0 @@
1804 1804 -e4ece1bf43360ddc8f6a96432201a37b7cd27ae4 s
1805 1805 -7af322bc1198a32402fe903e0b7ebcfc5c9bf8f4 t
1806 1806 $ hg log -r 8 --template "{p1node|short} {p2node|short}\n{subrepos % '{subrepo}\n'}"
1807 1807 f94576341bcf 000000000000
1808 1808
1809 1809 Test that '[paths]' is configured correctly at subrepo creation
1810 1810
1811 1811 $ cd $TESTTMP/tc
1812 1812 $ cat > .hgsub <<EOF
1813 1813 > # to clear bogus subrepo path 'bogus=[boguspath'
1814 1814 > s = s
1815 1815 > t = t
1816 1816 > EOF
1817 1817 $ hg update -q --clean null
1818 1818 $ rm -rf s t
1819 1819 $ cat >> .hg/hgrc <<EOF
1820 1820 > [paths]
1821 1821 > default-push = /foo/bar
1822 1822 > EOF
1823 1823 $ hg update -q
1824 1824 $ cat s/.hg/hgrc
1825 1825 [paths]
1826 1826 default = $TESTTMP/t/s
1827 1827 default-push = /foo/bar/s
1828 1828 $ cat s/ss/.hg/hgrc
1829 1829 [paths]
1830 1830 default = $TESTTMP/t/s/ss
1831 1831 default-push = /foo/bar/s/ss
1832 1832 $ cat t/.hg/hgrc
1833 1833 [paths]
1834 1834 default = $TESTTMP/t/t
1835 1835 default-push = /foo/bar/t
1836 1836
1837 1837 $ cd $TESTTMP/t
1838 1838 $ hg up -qC 0
1839 1839 $ echo 'bar' > bar.txt
1840 1840 $ hg ci -Am 'branch before subrepo add'
1841 1841 adding bar.txt
1842 1842 created new head
1843 1843 $ hg merge -r "first(subrepo('s'))"
1844 1844 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
1845 1845 (branch merge, don't forget to commit)
1846 1846 $ hg status -S -X '.hgsub*'
1847 1847 A s/a
1848 1848 ? s/b
1849 1849 ? s/c
1850 1850 ? s/f1
1851 1851 $ hg status -S --rev 'p2()'
1852 1852 A bar.txt
1853 1853 ? s/b
1854 1854 ? s/c
1855 1855 ? s/f1
1856 1856 $ hg diff -S -X '.hgsub*' --nodates
1857 1857 diff -r 000000000000 s/a
1858 1858 --- /dev/null
1859 1859 +++ b/s/a
1860 1860 @@ -0,0 +1,1 @@
1861 1861 +a
1862 1862 $ hg diff -S --rev 'p2()' --nodates
1863 1863 diff -r 7cf8cfea66e4 bar.txt
1864 1864 --- /dev/null
1865 1865 +++ b/bar.txt
1866 1866 @@ -0,0 +1,1 @@
1867 1867 +bar
1868 1868
1869 1869 $ cd ..
1870 1870
1871 1871 test for ssh exploit 2017-07-25
1872 1872
1873 1873 $ cat >> $HGRCPATH << EOF
1874 1874 > [ui]
1875 1875 > ssh = sh -c "read l; read l; read l"
1876 1876 > EOF
1877 1877
1878 1878 $ hg init malicious-proxycommand
1879 1879 $ cd malicious-proxycommand
1880 1880 $ echo 's = [hg]ssh://-oProxyCommand=touch${IFS}owned/path' > .hgsub
1881 1881 $ hg init s
1882 1882 $ cd s
1883 1883 $ echo init > init
1884 1884 $ hg add
1885 1885 adding init
1886 1886 $ hg commit -m init
1887 1887 $ cd ..
1888 1888 $ hg add .hgsub
1889 1889 $ hg ci -m 'add subrepo'
1890 1890 $ cd ..
1891 1891 $ hg clone malicious-proxycommand malicious-proxycommand-clone
1892 1892 updating to branch default
1893 1893 abort: potentially unsafe url: 'ssh://-oProxyCommand=touch${IFS}owned/path' (in subrepository "s")
1894 1894 [255]
1895 1895
1896 1896 also check that a percent encoded '-' (%2D) doesn't work
1897 1897
1898 1898 $ cd malicious-proxycommand
1899 1899 $ echo 's = [hg]ssh://%2DoProxyCommand=touch${IFS}owned/path' > .hgsub
1900 1900 $ hg ci -m 'change url to percent encoded'
1901 1901 $ cd ..
1902 1902 $ rm -r malicious-proxycommand-clone
1903 1903 $ hg clone malicious-proxycommand malicious-proxycommand-clone
1904 1904 updating to branch default
1905 1905 abort: potentially unsafe url: 'ssh://-oProxyCommand=touch${IFS}owned/path' (in subrepository "s")
1906 1906 [255]
1907 1907
1908 1908 also check for a pipe
1909 1909
1910 1910 $ cd malicious-proxycommand
1911 1911 $ echo 's = [hg]ssh://fakehost|touch${IFS}owned/path' > .hgsub
1912 1912 $ hg ci -m 'change url to pipe'
1913 1913 $ cd ..
1914 1914 $ rm -r malicious-proxycommand-clone
1915 1915 $ hg clone malicious-proxycommand malicious-proxycommand-clone
1916 1916 updating to branch default
1917 1917 abort: no suitable response from remote hg!
1918 1918 [255]
1919 1919 $ [ ! -f owned ] || echo 'you got owned'
1920 1920
1921 1921 also check that a percent encoded '|' (%7C) doesn't work
1922 1922
1923 1923 $ cd malicious-proxycommand
1924 1924 $ echo 's = [hg]ssh://fakehost%7Ctouch%20owned/path' > .hgsub
1925 1925 $ hg ci -m 'change url to percent encoded pipe'
1926 1926 $ cd ..
1927 1927 $ rm -r malicious-proxycommand-clone
1928 1928 $ hg clone malicious-proxycommand malicious-proxycommand-clone
1929 1929 updating to branch default
1930 1930 abort: no suitable response from remote hg!
1931 1931 [255]
1932 1932 $ [ ! -f owned ] || echo 'you got owned'
1933 1933
1934 1934 and bad usernames:
1935 1935 $ cd malicious-proxycommand
1936 1936 $ echo 's = [hg]ssh://-oProxyCommand=touch owned@example.com/path' > .hgsub
1937 1937 $ hg ci -m 'owned username'
1938 1938 $ cd ..
1939 1939 $ rm -r malicious-proxycommand-clone
1940 1940 $ hg clone malicious-proxycommand malicious-proxycommand-clone
1941 1941 updating to branch default
1942 1942 abort: potentially unsafe url: 'ssh://-oProxyCommand=touch owned@example.com/path' (in subrepository "s")
1943 1943 [255]
1944
1945 Test convert subrepositories including merge (issue5526):
1946
1947 $ hg init tconv
1948 $ hg convert --config extensions.convert= -q t/s tconv/s
1949 $ hg convert --config extensions.convert= -q t/s/ss tconv/s/ss
1950 $ hg convert --config extensions.convert= -q t/t tconv/t
1951
1952 convert shouldn't fail because of pseudo filenode:
1953
1954 $ hg convert --config extensions.convert= t tconv
1955 scanning source...
1956 sorting...
1957 converting...
1958 17 0
1959 16 1
1960 15 2
1961 14 3
1962 13 4
1963 12 5
1964 11 6
1965 10 7
1966 9 8
1967 8 9
1968 7 10
1969 6 11
1970 5 12
1971 4 13
1972 3 rm2
1973 2 phasecheck4
1974 1 16
1975 0 branch before subrepo add
1976
1977 converted .hgsubstate should point to valid nodes:
1978
1979 $ hg up -R tconv 9
1980 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
1981 $ cat tconv/.hgsubstate
1982 fc627a69481fcbe5f1135069e8a3881c023e4cf5 s
1983 60ca1237c19474e7a3978b0dc1ca4e6f36d51382 t
General Comments 0
You need to be logged in to leave comments. Login now