Failed recursive merging can silently lose date in StGIT
[stgit] / stgit / stack.py
1 """Basic quilt-like functionality
2 """
3
4 __copyright__ = """
5 Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
6
7 This program is free software; you can redistribute it and/or modify
8 it under the terms of the GNU General Public License version 2 as
9 published by the Free Software Foundation.
10
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with this program; if not, write to the Free Software
18 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19 """
20
21 import sys, os, re
22
23 from stgit.utils import *
24 from stgit import git, basedir, templates
25 from stgit.config import config
26
27
28 # stack exception class
29 class StackException(Exception):
30 pass
31
32 class FilterUntil:
33 def __init__(self):
34 self.should_print = True
35 def __call__(self, x, until_test, prefix):
36 if until_test(x):
37 self.should_print = False
38 if self.should_print:
39 return x[0:len(prefix)] != prefix
40 return False
41
42 #
43 # Functions
44 #
45 __comment_prefix = 'STG:'
46 __patch_prefix = 'STG_PATCH:'
47
48 def __clean_comments(f):
49 """Removes lines marked for status in a commit file
50 """
51 f.seek(0)
52
53 # remove status-prefixed lines
54 lines = f.readlines()
55
56 patch_filter = FilterUntil()
57 until_test = lambda t: t == (__patch_prefix + '\n')
58 lines = [l for l in lines if patch_filter(l, until_test, __comment_prefix)]
59
60 # remove empty lines at the end
61 while len(lines) != 0 and lines[-1] == '\n':
62 del lines[-1]
63
64 f.seek(0); f.truncate()
65 f.writelines(lines)
66
67 def edit_file(series, line, comment, show_patch = True):
68 fname = '.stgitmsg.txt'
69 tmpl = templates.get_template('patchdescr.tmpl')
70
71 f = file(fname, 'w+')
72 if line:
73 print >> f, line
74 elif tmpl:
75 print >> f, tmpl,
76 else:
77 print >> f
78 print >> f, __comment_prefix, comment
79 print >> f, __comment_prefix, \
80 'Lines prefixed with "%s" will be automatically removed.' \
81 % __comment_prefix
82 print >> f, __comment_prefix, \
83 'Trailing empty lines will be automatically removed.'
84
85 if show_patch:
86 print >> f, __patch_prefix
87 # series.get_patch(series.get_current()).get_top()
88 git.diff([], series.get_patch(series.get_current()).get_bottom(), None, f)
89
90 #Vim modeline must be near the end.
91 print >> f, __comment_prefix, 'vi: set textwidth=75 filetype=diff nobackup:'
92 f.close()
93
94 # the editor
95 editor = config.get('stgit.editor')
96 if editor:
97 pass
98 elif 'EDITOR' in os.environ:
99 editor = os.environ['EDITOR']
100 else:
101 editor = 'vi'
102 editor += ' %s' % fname
103
104 print 'Invoking the editor: "%s"...' % editor,
105 sys.stdout.flush()
106 print 'done (exit code: %d)' % os.system(editor)
107
108 f = file(fname, 'r+')
109
110 __clean_comments(f)
111 f.seek(0)
112 result = f.read()
113
114 f.close()
115 os.remove(fname)
116
117 return result
118
119 #
120 # Classes
121 #
122
123 class StgitObject:
124 """An object with stgit-like properties stored as files in a directory
125 """
126 def _set_dir(self, dir):
127 self.__dir = dir
128 def _dir(self):
129 return self.__dir
130
131 def create_empty_field(self, name):
132 create_empty_file(os.path.join(self.__dir, name))
133
134 def _get_field(self, name, multiline = False):
135 id_file = os.path.join(self.__dir, name)
136 if os.path.isfile(id_file):
137 line = read_string(id_file, multiline)
138 if line == '':
139 return None
140 else:
141 return line
142 else:
143 return None
144
145 def _set_field(self, name, value, multiline = False):
146 fname = os.path.join(self.__dir, name)
147 if value and value != '':
148 write_string(fname, value, multiline)
149 elif os.path.isfile(fname):
150 os.remove(fname)
151
152
153 class Patch(StgitObject):
154 """Basic patch implementation
155 """
156 def __init__(self, name, series_dir, refs_dir):
157 self.__series_dir = series_dir
158 self.__name = name
159 self._set_dir(os.path.join(self.__series_dir, self.__name))
160 self.__refs_dir = refs_dir
161 self.__top_ref_file = os.path.join(self.__refs_dir, self.__name)
162 self.__log_ref_file = os.path.join(self.__refs_dir,
163 self.__name + '.log')
164
165 def create(self):
166 os.mkdir(self._dir())
167 self.create_empty_field('bottom')
168 self.create_empty_field('top')
169
170 def delete(self):
171 for f in os.listdir(self._dir()):
172 os.remove(os.path.join(self._dir(), f))
173 os.rmdir(self._dir())
174 os.remove(self.__top_ref_file)
175 if os.path.exists(self.__log_ref_file):
176 os.remove(self.__log_ref_file)
177
178 def get_name(self):
179 return self.__name
180
181 def rename(self, newname):
182 olddir = self._dir()
183 old_top_ref_file = self.__top_ref_file
184 old_log_ref_file = self.__log_ref_file
185 self.__name = newname
186 self._set_dir(os.path.join(self.__series_dir, self.__name))
187 self.__top_ref_file = os.path.join(self.__refs_dir, self.__name)
188 self.__log_ref_file = os.path.join(self.__refs_dir,
189 self.__name + '.log')
190
191 os.rename(olddir, self._dir())
192 os.rename(old_top_ref_file, self.__top_ref_file)
193 if os.path.exists(old_log_ref_file):
194 os.rename(old_log_ref_file, self.__log_ref_file)
195
196 def __update_top_ref(self, ref):
197 write_string(self.__top_ref_file, ref)
198
199 def __update_log_ref(self, ref):
200 write_string(self.__log_ref_file, ref)
201
202 def update_top_ref(self):
203 top = self.get_top()
204 if top:
205 self.__update_top_ref(top)
206
207 def get_old_bottom(self):
208 return self._get_field('bottom.old')
209
210 def get_bottom(self):
211 return self._get_field('bottom')
212
213 def set_bottom(self, value, backup = False):
214 if backup:
215 curr = self._get_field('bottom')
216 self._set_field('bottom.old', curr)
217 self._set_field('bottom', value)
218
219 def get_old_top(self):
220 return self._get_field('top.old')
221
222 def get_top(self):
223 return self._get_field('top')
224
225 def set_top(self, value, backup = False):
226 if backup:
227 curr = self._get_field('top')
228 self._set_field('top.old', curr)
229 self._set_field('top', value)
230 self.__update_top_ref(value)
231
232 def restore_old_boundaries(self):
233 bottom = self._get_field('bottom.old')
234 top = self._get_field('top.old')
235
236 if top and bottom:
237 self._set_field('bottom', bottom)
238 self._set_field('top', top)
239 self.__update_top_ref(top)
240 return True
241 else:
242 return False
243
244 def get_description(self):
245 return self._get_field('description', True)
246
247 def set_description(self, line):
248 self._set_field('description', line, True)
249
250 def get_authname(self):
251 return self._get_field('authname')
252
253 def set_authname(self, name):
254 self._set_field('authname', name or git.author().name)
255
256 def get_authemail(self):
257 return self._get_field('authemail')
258
259 def set_authemail(self, email):
260 self._set_field('authemail', email or git.author().email)
261
262 def get_authdate(self):
263 return self._get_field('authdate')
264
265 def set_authdate(self, date):
266 self._set_field('authdate', date or git.author().date)
267
268 def get_commname(self):
269 return self._get_field('commname')
270
271 def set_commname(self, name):
272 self._set_field('commname', name or git.committer().name)
273
274 def get_commemail(self):
275 return self._get_field('commemail')
276
277 def set_commemail(self, email):
278 self._set_field('commemail', email or git.committer().email)
279
280 def get_log(self):
281 return self._get_field('log')
282
283 def set_log(self, value, backup = False):
284 self._set_field('log', value)
285 self.__update_log_ref(value)
286
287
288 class Series(StgitObject):
289 """Class including the operations on series
290 """
291 def __init__(self, name = None):
292 """Takes a series name as the parameter.
293 """
294 try:
295 if name:
296 self.__name = name
297 else:
298 self.__name = git.get_head_file()
299 self.__base_dir = basedir.get()
300 except git.GitException, ex:
301 raise StackException, 'GIT tree not initialised: %s' % ex
302
303 self._set_dir(os.path.join(self.__base_dir, 'patches', self.__name))
304 self.__refs_dir = os.path.join(self.__base_dir, 'refs', 'patches',
305 self.__name)
306 self.__base_file = os.path.join(self.__base_dir, 'refs', 'bases',
307 self.__name)
308
309 self.__applied_file = os.path.join(self._dir(), 'applied')
310 self.__unapplied_file = os.path.join(self._dir(), 'unapplied')
311 self.__hidden_file = os.path.join(self._dir(), 'hidden')
312 self.__current_file = os.path.join(self._dir(), 'current')
313 self.__descr_file = os.path.join(self._dir(), 'description')
314
315 # where this series keeps its patches
316 self.__patch_dir = os.path.join(self._dir(), 'patches')
317 if not os.path.isdir(self.__patch_dir):
318 self.__patch_dir = self._dir()
319
320 # if no __refs_dir, create and populate it (upgrade old repositories)
321 if self.is_initialised() and not os.path.isdir(self.__refs_dir):
322 os.makedirs(self.__refs_dir)
323 for patch in self.get_applied() + self.get_unapplied():
324 self.get_patch(patch).update_top_ref()
325
326 # trash directory
327 self.__trash_dir = os.path.join(self._dir(), 'trash')
328 if self.is_initialised() and not os.path.isdir(self.__trash_dir):
329 os.makedirs(self.__trash_dir)
330
331 def __patch_name_valid(self, name):
332 """Raise an exception if the patch name is not valid.
333 """
334 if not name or re.search('[^\w.-]', name):
335 raise StackException, 'Invalid patch name: "%s"' % name
336
337 def get_branch(self):
338 """Return the branch name for the Series object
339 """
340 return self.__name
341
342 def __set_current(self, name):
343 """Sets the topmost patch
344 """
345 self._set_field('current', name)
346
347 def get_patch(self, name):
348 """Return a Patch object for the given name
349 """
350 return Patch(name, self.__patch_dir, self.__refs_dir)
351
352 def get_current_patch(self):
353 """Return a Patch object representing the topmost patch, or
354 None if there is no such patch."""
355 crt = self.get_current()
356 if not crt:
357 return None
358 return Patch(crt, self.__patch_dir, self.__refs_dir)
359
360 def get_current(self):
361 """Return the name of the topmost patch, or None if there is
362 no such patch."""
363 name = self._get_field('current')
364 if name == '':
365 return None
366 else:
367 return name
368
369 def get_applied(self):
370 if not os.path.isfile(self.__applied_file):
371 raise StackException, 'Branch "%s" not initialised' % self.__name
372 f = file(self.__applied_file)
373 names = [line.strip() for line in f.readlines()]
374 f.close()
375 return names
376
377 def get_unapplied(self):
378 if not os.path.isfile(self.__unapplied_file):
379 raise StackException, 'Branch "%s" not initialised' % self.__name
380 f = file(self.__unapplied_file)
381 names = [line.strip() for line in f.readlines()]
382 f.close()
383 return names
384
385 def get_hidden(self):
386 if not os.path.isfile(self.__hidden_file):
387 return []
388 f = file(self.__hidden_file)
389 names = [line.strip() for line in f.readlines()]
390 f.close()
391 return names
392
393 def get_base_file(self):
394 self.__begin_stack_check()
395 return self.__base_file
396
397 def get_base(self):
398 return read_string(self.get_base_file())
399
400 def get_protected(self):
401 return os.path.isfile(os.path.join(self._dir(), 'protected'))
402
403 def protect(self):
404 protect_file = os.path.join(self._dir(), 'protected')
405 if not os.path.isfile(protect_file):
406 create_empty_file(protect_file)
407
408 def unprotect(self):
409 protect_file = os.path.join(self._dir(), 'protected')
410 if os.path.isfile(protect_file):
411 os.remove(protect_file)
412
413 def get_description(self):
414 return self._get_field('description') or ''
415
416 def set_description(self, line):
417 self._set_field('description', line)
418
419 def get_parent_remote(self):
420 value = config.get('branch.%s.remote' % self.__name)
421 if value:
422 return value
423 elif 'origin' in git.remotes_list():
424 print 'Notice: no parent remote declared for stack "%s", ' \
425 'defaulting to "origin". Consider setting "branch.%s.remote" ' \
426 'and "branch.%s.merge" with "git repo-config".' \
427 % (self.__name, self.__name, self.__name)
428 return 'origin'
429 else:
430 raise StackException, 'Cannot find a parent remote for "%s"' % self.__name
431
432 def __set_parent_remote(self, remote):
433 value = config.set('branch.%s.remote' % self.__name, remote)
434
435 def get_parent_branch(self):
436 value = config.get('branch.%s.stgit.parentbranch' % self.__name)
437 if value:
438 return value
439 elif git.rev_parse('heads/origin'):
440 print 'Notice: no parent branch declared for stack "%s", ' \
441 'defaulting to "heads/origin". Consider setting ' \
442 '"branch.%s.stgit.parentbranch" with "git repo-config".' \
443 % (self.__name, self.__name)
444 return 'heads/origin'
445 else:
446 raise StackException, 'Cannot find a parent branch for "%s"' % self.__name
447
448 def __set_parent_branch(self, name):
449 if config.get('branch.%s.remote' % self.__name):
450 # Never set merge if remote is not set to avoid
451 # possibly-erroneous lookups into 'origin'
452 config.set('branch.%s.merge' % self.__name, name)
453 config.set('branch.%s.stgit.parentbranch' % self.__name, name)
454
455 def set_parent(self, remote, localbranch):
456 # policy: record local branches as remote='.'
457 recordremote = remote or '.'
458 if localbranch:
459 self.__set_parent_remote(recordremote)
460 self.__set_parent_branch(localbranch)
461 # We'll enforce this later
462 # else:
463 # raise StackException, 'Parent branch (%s) should be specified for %s' % localbranch, self.__name
464
465 def __patch_is_current(self, patch):
466 return patch.get_name() == self.get_current()
467
468 def patch_applied(self, name):
469 """Return true if the patch exists in the applied list
470 """
471 return name in self.get_applied()
472
473 def patch_unapplied(self, name):
474 """Return true if the patch exists in the unapplied list
475 """
476 return name in self.get_unapplied()
477
478 def patch_hidden(self, name):
479 """Return true if the patch is hidden.
480 """
481 return name in self.get_hidden()
482
483 def patch_exists(self, name):
484 """Return true if there is a patch with the given name, false
485 otherwise."""
486 return self.patch_applied(name) or self.patch_unapplied(name)
487
488 def __begin_stack_check(self):
489 """Save the current HEAD into .git/refs/heads/base if the stack
490 is empty
491 """
492 if len(self.get_applied()) == 0:
493 head = git.get_head()
494 write_string(self.__base_file, head)
495
496 def __end_stack_check(self):
497 """Remove .git/refs/heads/base if the stack is empty.
498 This warning should never happen
499 """
500 if len(self.get_applied()) == 0 \
501 and read_string(self.__base_file) != git.get_head():
502 print 'Warning: stack empty but the HEAD and base are different'
503
504 def head_top_equal(self):
505 """Return true if the head and the top are the same
506 """
507 crt = self.get_current_patch()
508 if not crt:
509 # we don't care, no patches applied
510 return True
511 return git.get_head() == crt.get_top()
512
513 def is_initialised(self):
514 """Checks if series is already initialised
515 """
516 return os.path.isdir(self.__patch_dir)
517
518 def init(self, create_at=False, parent_remote=None, parent_branch=None):
519 """Initialises the stgit series
520 """
521 if os.path.exists(self.__patch_dir):
522 raise StackException, self.__patch_dir + ' already exists'
523 if os.path.exists(self.__refs_dir):
524 raise StackException, self.__refs_dir + ' already exists'
525 if os.path.exists(self.__base_file):
526 raise StackException, self.__base_file + ' already exists'
527
528 if (create_at!=False):
529 git.create_branch(self.__name, create_at)
530
531 os.makedirs(self.__patch_dir)
532
533 self.set_parent(parent_remote, parent_branch)
534
535 create_dirs(os.path.join(self.__base_dir, 'refs', 'bases'))
536
537 self.create_empty_field('applied')
538 self.create_empty_field('unapplied')
539 self.create_empty_field('description')
540 os.makedirs(os.path.join(self._dir(), 'patches'))
541 os.makedirs(self.__refs_dir)
542 self.__begin_stack_check()
543 self._set_field('orig-base', git.get_head())
544
545 def convert(self):
546 """Either convert to use a separate patch directory, or
547 unconvert to place the patches in the same directory with
548 series control files
549 """
550 if self.__patch_dir == self._dir():
551 print 'Converting old-style to new-style...',
552 sys.stdout.flush()
553
554 self.__patch_dir = os.path.join(self._dir(), 'patches')
555 os.makedirs(self.__patch_dir)
556
557 for p in self.get_applied() + self.get_unapplied():
558 src = os.path.join(self._dir(), p)
559 dest = os.path.join(self.__patch_dir, p)
560 os.rename(src, dest)
561
562 print 'done'
563
564 else:
565 print 'Converting new-style to old-style...',
566 sys.stdout.flush()
567
568 for p in self.get_applied() + self.get_unapplied():
569 src = os.path.join(self.__patch_dir, p)
570 dest = os.path.join(self._dir(), p)
571 os.rename(src, dest)
572
573 if not os.listdir(self.__patch_dir):
574 os.rmdir(self.__patch_dir)
575 print 'done'
576 else:
577 print 'Patch directory %s is not empty.' % self.__name
578
579 self.__patch_dir = self._dir()
580
581 def rename(self, to_name):
582 """Renames a series
583 """
584 to_stack = Series(to_name)
585
586 if to_stack.is_initialised():
587 raise StackException, '"%s" already exists' % to_stack.get_branch()
588 if os.path.exists(to_stack.__base_file):
589 os.remove(to_stack.__base_file)
590
591 git.rename_branch(self.__name, to_name)
592
593 if os.path.isdir(self._dir()):
594 rename(os.path.join(self.__base_dir, 'patches'),
595 self.__name, to_stack.__name)
596 if os.path.exists(self.__base_file):
597 rename(os.path.join(self.__base_dir, 'refs', 'bases'),
598 self.__name, to_stack.__name)
599 if os.path.exists(self.__refs_dir):
600 rename(os.path.join(self.__base_dir, 'refs', 'patches'),
601 self.__name, to_stack.__name)
602
603 # Rename the config section
604 config.rename_section("branch.%s" % self.__name,
605 "branch.%s" % to_name)
606
607 self.__init__(to_name)
608
609 def clone(self, target_series):
610 """Clones a series
611 """
612 try:
613 # allow cloning of branches not under StGIT control
614 base = self.get_base()
615 except:
616 base = git.get_head()
617 Series(target_series).init(create_at = base)
618 new_series = Series(target_series)
619
620 # generate an artificial description file
621 new_series.set_description('clone of "%s"' % self.__name)
622
623 # clone self's entire series as unapplied patches
624 try:
625 # allow cloning of branches not under StGIT control
626 applied = self.get_applied()
627 unapplied = self.get_unapplied()
628 patches = applied + unapplied
629 patches.reverse()
630 except:
631 patches = applied = unapplied = []
632 for p in patches:
633 patch = self.get_patch(p)
634 new_series.new_patch(p, message = patch.get_description(),
635 can_edit = False, unapplied = True,
636 bottom = patch.get_bottom(),
637 top = patch.get_top(),
638 author_name = patch.get_authname(),
639 author_email = patch.get_authemail(),
640 author_date = patch.get_authdate())
641
642 # fast forward the cloned series to self's top
643 new_series.forward_patches(applied)
644
645 # Clone remote and merge settings
646 value = config.get('branch.%s.remote' % self.__name)
647 if value:
648 config.set('branch.%s.remote' % target_series, value)
649
650 value = config.get('branch.%s.merge' % self.__name)
651 if value:
652 config.set('branch.%s.merge' % target_series, value)
653
654 def delete(self, force = False):
655 """Deletes an stgit series
656 """
657 if self.is_initialised():
658 patches = self.get_unapplied() + self.get_applied()
659 if not force and patches:
660 raise StackException, \
661 'Cannot delete: the series still contains patches'
662 for p in patches:
663 Patch(p, self.__patch_dir, self.__refs_dir).delete()
664
665 # remove the trash directory
666 for fname in os.listdir(self.__trash_dir):
667 os.remove(fname)
668 os.rmdir(self.__trash_dir)
669
670 # FIXME: find a way to get rid of those manual removals
671 # (move functionality to StgitObject ?)
672 if os.path.exists(self.__applied_file):
673 os.remove(self.__applied_file)
674 if os.path.exists(self.__unapplied_file):
675 os.remove(self.__unapplied_file)
676 if os.path.exists(self.__hidden_file):
677 os.remove(self.__hidden_file)
678 if os.path.exists(self.__current_file):
679 os.remove(self.__current_file)
680 if os.path.exists(self.__descr_file):
681 os.remove(self.__descr_file)
682 if not os.listdir(self.__patch_dir):
683 os.rmdir(self.__patch_dir)
684 else:
685 print 'Patch directory %s is not empty.' % self.__name
686 if not os.listdir(self._dir()):
687 remove_dirs(os.path.join(self.__base_dir, 'patches'),
688 self.__name)
689 else:
690 print 'Series directory %s is not empty.' % self.__name
691 if not os.listdir(self.__refs_dir):
692 remove_dirs(os.path.join(self.__base_dir, 'refs', 'patches'),
693 self.__name)
694 else:
695 print 'Refs directory %s is not empty.' % self.__refs_dir
696
697 if os.path.exists(self.__base_file):
698 remove_file_and_dirs(
699 os.path.join(self.__base_dir, 'refs', 'bases'), self.__name)
700
701 def refresh_patch(self, files = None, message = None, edit = False,
702 show_patch = False,
703 cache_update = True,
704 author_name = None, author_email = None,
705 author_date = None,
706 committer_name = None, committer_email = None,
707 backup = False, sign_str = None, log = 'refresh'):
708 """Generates a new commit for the given patch
709 """
710 name = self.get_current()
711 if not name:
712 raise StackException, 'No patches applied'
713
714 patch = Patch(name, self.__patch_dir, self.__refs_dir)
715
716 descr = patch.get_description()
717 if not (message or descr):
718 edit = True
719 descr = ''
720 elif message:
721 descr = message
722
723 if not message and edit:
724 descr = edit_file(self, descr.rstrip(), \
725 'Please edit the description for patch "%s" ' \
726 'above.' % name, show_patch)
727
728 if not author_name:
729 author_name = patch.get_authname()
730 if not author_email:
731 author_email = patch.get_authemail()
732 if not author_date:
733 author_date = patch.get_authdate()
734 if not committer_name:
735 committer_name = patch.get_commname()
736 if not committer_email:
737 committer_email = patch.get_commemail()
738
739 if sign_str:
740 descr = '%s\n%s: %s <%s>\n' % (descr.rstrip(), sign_str,
741 committer_name, committer_email)
742
743 bottom = patch.get_bottom()
744
745 commit_id = git.commit(files = files,
746 message = descr, parents = [bottom],
747 cache_update = cache_update,
748 allowempty = True,
749 author_name = author_name,
750 author_email = author_email,
751 author_date = author_date,
752 committer_name = committer_name,
753 committer_email = committer_email)
754
755 patch.set_bottom(bottom, backup = backup)
756 patch.set_top(commit_id, backup = backup)
757 patch.set_description(descr)
758 patch.set_authname(author_name)
759 patch.set_authemail(author_email)
760 patch.set_authdate(author_date)
761 patch.set_commname(committer_name)
762 patch.set_commemail(committer_email)
763
764 if log:
765 self.log_patch(patch, log)
766
767 return commit_id
768
769 def undo_refresh(self):
770 """Undo the patch boundaries changes caused by 'refresh'
771 """
772 name = self.get_current()
773 assert(name)
774
775 patch = Patch(name, self.__patch_dir, self.__refs_dir)
776 old_bottom = patch.get_old_bottom()
777 old_top = patch.get_old_top()
778
779 # the bottom of the patch is not changed by refresh. If the
780 # old_bottom is different, there wasn't any previous 'refresh'
781 # command (probably only a 'push')
782 if old_bottom != patch.get_bottom() or old_top == patch.get_top():
783 raise StackException, 'No undo information available'
784
785 git.reset(tree_id = old_top, check_out = False)
786 if patch.restore_old_boundaries():
787 self.log_patch(patch, 'undo')
788
789 def new_patch(self, name, message = None, can_edit = True,
790 unapplied = False, show_patch = False,
791 top = None, bottom = None,
792 author_name = None, author_email = None, author_date = None,
793 committer_name = None, committer_email = None,
794 before_existing = False, refresh = True):
795 """Creates a new patch
796 """
797 self.__patch_name_valid(name)
798
799 if self.patch_applied(name) or self.patch_unapplied(name):
800 raise StackException, 'Patch "%s" already exists' % name
801
802 if not message and can_edit:
803 descr = edit_file(self, None, \
804 'Please enter the description for patch "%s" ' \
805 'above.' % name, show_patch)
806 else:
807 descr = message
808
809 head = git.get_head()
810
811 self.__begin_stack_check()
812
813 patch = Patch(name, self.__patch_dir, self.__refs_dir)
814 patch.create()
815
816 if bottom:
817 patch.set_bottom(bottom)
818 else:
819 patch.set_bottom(head)
820 if top:
821 patch.set_top(top)
822 else:
823 patch.set_top(head)
824
825 patch.set_description(descr)
826 patch.set_authname(author_name)
827 patch.set_authemail(author_email)
828 patch.set_authdate(author_date)
829 patch.set_commname(committer_name)
830 patch.set_commemail(committer_email)
831
832 if unapplied:
833 self.log_patch(patch, 'new')
834
835 patches = [patch.get_name()] + self.get_unapplied()
836
837 f = file(self.__unapplied_file, 'w+')
838 f.writelines([line + '\n' for line in patches])
839 f.close()
840 elif before_existing:
841 self.log_patch(patch, 'new')
842
843 insert_string(self.__applied_file, patch.get_name())
844 if not self.get_current():
845 self.__set_current(name)
846 else:
847 append_string(self.__applied_file, patch.get_name())
848 self.__set_current(name)
849 if refresh:
850 self.refresh_patch(cache_update = False, log = 'new')
851
852 def delete_patch(self, name):
853 """Deletes a patch
854 """
855 self.__patch_name_valid(name)
856 patch = Patch(name, self.__patch_dir, self.__refs_dir)
857
858 if self.__patch_is_current(patch):
859 self.pop_patch(name)
860 elif self.patch_applied(name):
861 raise StackException, 'Cannot remove an applied patch, "%s", ' \
862 'which is not current' % name
863 elif not name in self.get_unapplied():
864 raise StackException, 'Unknown patch "%s"' % name
865
866 # save the commit id to a trash file
867 write_string(os.path.join(self.__trash_dir, name), patch.get_top())
868
869 patch.delete()
870
871 unapplied = self.get_unapplied()
872 unapplied.remove(name)
873 f = file(self.__unapplied_file, 'w+')
874 f.writelines([line + '\n' for line in unapplied])
875 f.close()
876
877 if self.patch_hidden(name):
878 self.unhide_patch(name)
879
880 self.__begin_stack_check()
881
882 def forward_patches(self, names):
883 """Try to fast-forward an array of patches.
884
885 On return, patches in names[0:returned_value] have been pushed on the
886 stack. Apply the rest with push_patch
887 """
888 unapplied = self.get_unapplied()
889 self.__begin_stack_check()
890
891 forwarded = 0
892 top = git.get_head()
893
894 for name in names:
895 assert(name in unapplied)
896
897 patch = Patch(name, self.__patch_dir, self.__refs_dir)
898
899 head = top
900 bottom = patch.get_bottom()
901 top = patch.get_top()
902
903 # top != bottom always since we have a commit for each patch
904 if head == bottom:
905 # reset the backup information. No logging since the
906 # patch hasn't changed
907 patch.set_bottom(head, backup = True)
908 patch.set_top(top, backup = True)
909
910 else:
911 head_tree = git.get_commit(head).get_tree()
912 bottom_tree = git.get_commit(bottom).get_tree()
913 if head_tree == bottom_tree:
914 # We must just reparent this patch and create a new commit
915 # for it
916 descr = patch.get_description()
917 author_name = patch.get_authname()
918 author_email = patch.get_authemail()
919 author_date = patch.get_authdate()
920 committer_name = patch.get_commname()
921 committer_email = patch.get_commemail()
922
923 top_tree = git.get_commit(top).get_tree()
924
925 top = git.commit(message = descr, parents = [head],
926 cache_update = False,
927 tree_id = top_tree,
928 allowempty = True,
929 author_name = author_name,
930 author_email = author_email,
931 author_date = author_date,
932 committer_name = committer_name,
933 committer_email = committer_email)
934
935 patch.set_bottom(head, backup = True)
936 patch.set_top(top, backup = True)
937
938 self.log_patch(patch, 'push(f)')
939 else:
940 top = head
941 # stop the fast-forwarding, must do a real merge
942 break
943
944 forwarded+=1
945 unapplied.remove(name)
946
947 if forwarded == 0:
948 return 0
949
950 git.switch(top)
951
952 append_strings(self.__applied_file, names[0:forwarded])
953
954 f = file(self.__unapplied_file, 'w+')
955 f.writelines([line + '\n' for line in unapplied])
956 f.close()
957
958 self.__set_current(name)
959
960 return forwarded
961
962 def merged_patches(self, names):
963 """Test which patches were merged upstream by reverse-applying
964 them in reverse order. The function returns the list of
965 patches detected to have been applied. The state of the tree
966 is restored to the original one
967 """
968 patches = [Patch(name, self.__patch_dir, self.__refs_dir)
969 for name in names]
970 patches.reverse()
971
972 merged = []
973 for p in patches:
974 if git.apply_diff(p.get_top(), p.get_bottom()):
975 merged.append(p.get_name())
976 merged.reverse()
977
978 git.reset()
979
980 return merged
981
982 def push_patch(self, name, empty = False):
983 """Pushes a patch on the stack
984 """
985 unapplied = self.get_unapplied()
986 assert(name in unapplied)
987
988 self.__begin_stack_check()
989
990 patch = Patch(name, self.__patch_dir, self.__refs_dir)
991
992 head = git.get_head()
993 bottom = patch.get_bottom()
994 top = patch.get_top()
995
996 ex = None
997 modified = False
998
999 # top != bottom always since we have a commit for each patch
1000 if empty:
1001 # just make an empty patch (top = bottom = HEAD). This
1002 # option is useful to allow undoing already merged
1003 # patches. The top is updated by refresh_patch since we
1004 # need an empty commit
1005 patch.set_bottom(head, backup = True)
1006 patch.set_top(head, backup = True)
1007 modified = True
1008 elif head == bottom:
1009 # reset the backup information. No need for logging
1010 patch.set_bottom(bottom, backup = True)
1011 patch.set_top(top, backup = True)
1012
1013 git.switch(top)
1014 else:
1015 # new patch needs to be refreshed.
1016 # The current patch is empty after merge.
1017 patch.set_bottom(head, backup = True)
1018 patch.set_top(head, backup = True)
1019
1020 # Try the fast applying first. If this fails, fall back to the
1021 # three-way merge
1022 if not git.apply_diff(bottom, top):
1023 # if git.apply_diff() fails, the patch requires a diff3
1024 # merge and can be reported as modified
1025 modified = True
1026
1027 # merge can fail but the patch needs to be pushed
1028 try:
1029 git.merge(bottom, head, top, recursive = True)
1030 except git.GitException, ex:
1031 print >> sys.stderr, \
1032 'The merge failed during "push". ' \
1033 'Use "refresh" after fixing the conflicts or ' \
1034 'revert the operation with "push --undo".'
1035
1036 append_string(self.__applied_file, name)
1037
1038 unapplied.remove(name)
1039 f = file(self.__unapplied_file, 'w+')
1040 f.writelines([line + '\n' for line in unapplied])
1041 f.close()
1042
1043 self.__set_current(name)
1044
1045 # head == bottom case doesn't need to refresh the patch
1046 if empty or head != bottom:
1047 if not ex:
1048 # if the merge was OK and no conflicts, just refresh the patch
1049 # The GIT cache was already updated by the merge operation
1050 if modified:
1051 log = 'push(m)'
1052 else:
1053 log = 'push'
1054 self.refresh_patch(cache_update = False, log = log)
1055 else:
1056 # we store the correctly merged files only for
1057 # tracking the conflict history. Note that the
1058 # git.merge() operations should always leave the index
1059 # in a valid state (i.e. only stage 0 files)
1060 self.refresh_patch(cache_update = False, log = 'push(c)')
1061 raise StackException, str(ex)
1062
1063 return modified
1064
1065 def undo_push(self):
1066 name = self.get_current()
1067 assert(name)
1068
1069 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1070 old_bottom = patch.get_old_bottom()
1071 old_top = patch.get_old_top()
1072
1073 # the top of the patch is changed by a push operation only
1074 # together with the bottom (otherwise the top was probably
1075 # modified by 'refresh'). If they are both unchanged, there
1076 # was a fast forward
1077 if old_bottom == patch.get_bottom() and old_top != patch.get_top():
1078 raise StackException, 'No undo information available'
1079
1080 git.reset()
1081 self.pop_patch(name)
1082 ret = patch.restore_old_boundaries()
1083 if ret:
1084 self.log_patch(patch, 'undo')
1085
1086 return ret
1087
1088 def pop_patch(self, name, keep = False):
1089 """Pops the top patch from the stack
1090 """
1091 applied = self.get_applied()
1092 applied.reverse()
1093 assert(name in applied)
1094
1095 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1096
1097 # only keep the local changes
1098 if keep and not git.apply_diff(git.get_head(), patch.get_bottom()):
1099 raise StackException, \
1100 'Failed to pop patches while preserving the local changes'
1101
1102 git.switch(patch.get_bottom(), keep)
1103
1104 # save the new applied list
1105 idx = applied.index(name) + 1
1106
1107 popped = applied[:idx]
1108 popped.reverse()
1109 unapplied = popped + self.get_unapplied()
1110
1111 f = file(self.__unapplied_file, 'w+')
1112 f.writelines([line + '\n' for line in unapplied])
1113 f.close()
1114
1115 del applied[:idx]
1116 applied.reverse()
1117
1118 f = file(self.__applied_file, 'w+')
1119 f.writelines([line + '\n' for line in applied])
1120 f.close()
1121
1122 if applied == []:
1123 self.__set_current(None)
1124 else:
1125 self.__set_current(applied[-1])
1126
1127 self.__end_stack_check()
1128
1129 def empty_patch(self, name):
1130 """Returns True if the patch is empty
1131 """
1132 self.__patch_name_valid(name)
1133 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1134 bottom = patch.get_bottom()
1135 top = patch.get_top()
1136
1137 if bottom == top:
1138 return True
1139 elif git.get_commit(top).get_tree() \
1140 == git.get_commit(bottom).get_tree():
1141 return True
1142
1143 return False
1144
1145 def rename_patch(self, oldname, newname):
1146 self.__patch_name_valid(newname)
1147
1148 applied = self.get_applied()
1149 unapplied = self.get_unapplied()
1150
1151 if oldname == newname:
1152 raise StackException, '"To" name and "from" name are the same'
1153
1154 if newname in applied or newname in unapplied:
1155 raise StackException, 'Patch "%s" already exists' % newname
1156
1157 if self.patch_hidden(oldname):
1158 self.unhide_patch(oldname)
1159 self.hide_patch(newname)
1160
1161 if oldname in unapplied:
1162 Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1163 unapplied[unapplied.index(oldname)] = newname
1164
1165 f = file(self.__unapplied_file, 'w+')
1166 f.writelines([line + '\n' for line in unapplied])
1167 f.close()
1168 elif oldname in applied:
1169 Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1170 if oldname == self.get_current():
1171 self.__set_current(newname)
1172
1173 applied[applied.index(oldname)] = newname
1174
1175 f = file(self.__applied_file, 'w+')
1176 f.writelines([line + '\n' for line in applied])
1177 f.close()
1178 else:
1179 raise StackException, 'Unknown patch "%s"' % oldname
1180
1181 def log_patch(self, patch, message):
1182 """Generate a log commit for a patch
1183 """
1184 top = git.get_commit(patch.get_top())
1185 msg = '%s\t%s' % (message, top.get_id_hash())
1186
1187 old_log = patch.get_log()
1188 if old_log:
1189 parents = [old_log]
1190 else:
1191 parents = []
1192
1193 log = git.commit(message = msg, parents = parents,
1194 cache_update = False, tree_id = top.get_tree(),
1195 allowempty = True)
1196 patch.set_log(log)
1197
1198 def hide_patch(self, name):
1199 """Add the patch to the hidden list.
1200 """
1201 if not self.patch_exists(name):
1202 raise StackException, 'Unknown patch "%s"' % name
1203 elif self.patch_hidden(name):
1204 raise StackException, 'Patch "%s" already hidden' % name
1205
1206 append_string(self.__hidden_file, name)
1207
1208 def unhide_patch(self, name):
1209 """Add the patch to the hidden list.
1210 """
1211 if not self.patch_exists(name):
1212 raise StackException, 'Unknown patch "%s"' % name
1213 hidden = self.get_hidden()
1214 if not name in hidden:
1215 raise StackException, 'Patch "%s" not hidden' % name
1216
1217 hidden.remove(name)
1218
1219 f = file(self.__hidden_file, 'w+')
1220 f.writelines([line + '\n' for line in hidden])
1221 f.close()