Move identification of parent branch's remote def up into stack class.
[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
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 get_branch(self):
332 """Return the branch name for the Series object
333 """
334 return self.__name
335
336 def __set_current(self, name):
337 """Sets the topmost patch
338 """
339 self._set_field('current', name)
340
341 def get_patch(self, name):
342 """Return a Patch object for the given name
343 """
344 return Patch(name, self.__patch_dir, self.__refs_dir)
345
346 def get_current_patch(self):
347 """Return a Patch object representing the topmost patch, or
348 None if there is no such patch."""
349 crt = self.get_current()
350 if not crt:
351 return None
352 return Patch(crt, self.__patch_dir, self.__refs_dir)
353
354 def get_current(self):
355 """Return the name of the topmost patch, or None if there is
356 no such patch."""
357 name = self._get_field('current')
358 if name == '':
359 return None
360 else:
361 return name
362
363 def get_applied(self):
364 if not os.path.isfile(self.__applied_file):
365 raise StackException, 'Branch "%s" not initialised' % self.__name
366 f = file(self.__applied_file)
367 names = [line.strip() for line in f.readlines()]
368 f.close()
369 return names
370
371 def get_unapplied(self):
372 if not os.path.isfile(self.__unapplied_file):
373 raise StackException, 'Branch "%s" not initialised' % self.__name
374 f = file(self.__unapplied_file)
375 names = [line.strip() for line in f.readlines()]
376 f.close()
377 return names
378
379 def get_hidden(self):
380 if not os.path.isfile(self.__hidden_file):
381 return []
382 f = file(self.__hidden_file)
383 names = [line.strip() for line in f.readlines()]
384 f.close()
385 return names
386
387 def get_base_file(self):
388 self.__begin_stack_check()
389 return self.__base_file
390
391 def get_protected(self):
392 return os.path.isfile(os.path.join(self._dir(), 'protected'))
393
394 def protect(self):
395 protect_file = os.path.join(self._dir(), 'protected')
396 if not os.path.isfile(protect_file):
397 create_empty_file(protect_file)
398
399 def unprotect(self):
400 protect_file = os.path.join(self._dir(), 'protected')
401 if os.path.isfile(protect_file):
402 os.remove(protect_file)
403
404 def get_description(self):
405 return self._get_field('description') or ''
406
407 def set_description(self, line):
408 self._set_field('description', line)
409
410 def get_parent_remote(self):
411 return config.get('branch.%s.remote' % self.__name) or 'origin'
412
413 def __set_parent_remote(self, remote):
414 value = config.set('branch.%s.remote' % self.__name, remote)
415
416 def __patch_is_current(self, patch):
417 return patch.get_name() == self.get_current()
418
419 def patch_applied(self, name):
420 """Return true if the patch exists in the applied list
421 """
422 return name in self.get_applied()
423
424 def patch_unapplied(self, name):
425 """Return true if the patch exists in the unapplied list
426 """
427 return name in self.get_unapplied()
428
429 def patch_hidden(self, name):
430 """Return true if the patch is hidden.
431 """
432 return name in self.get_hidden()
433
434 def patch_exists(self, name):
435 """Return true if there is a patch with the given name, false
436 otherwise."""
437 return self.patch_applied(name) or self.patch_unapplied(name)
438
439 def __begin_stack_check(self):
440 """Save the current HEAD into .git/refs/heads/base if the stack
441 is empty
442 """
443 if len(self.get_applied()) == 0:
444 head = git.get_head()
445 write_string(self.__base_file, head)
446
447 def __end_stack_check(self):
448 """Remove .git/refs/heads/base if the stack is empty.
449 This warning should never happen
450 """
451 if len(self.get_applied()) == 0 \
452 and read_string(self.__base_file) != git.get_head():
453 print 'Warning: stack empty but the HEAD and base are different'
454
455 def head_top_equal(self):
456 """Return true if the head and the top are the same
457 """
458 crt = self.get_current_patch()
459 if not crt:
460 # we don't care, no patches applied
461 return True
462 return git.get_head() == crt.get_top()
463
464 def is_initialised(self):
465 """Checks if series is already initialised
466 """
467 return os.path.isdir(self.__patch_dir)
468
469 def init(self, create_at=False):
470 """Initialises the stgit series
471 """
472 bases_dir = os.path.join(self.__base_dir, 'refs', 'bases')
473
474 if os.path.exists(self.__patch_dir):
475 raise StackException, self.__patch_dir + ' already exists'
476 if os.path.exists(self.__refs_dir):
477 raise StackException, self.__refs_dir + ' already exists'
478 if os.path.exists(self.__base_file):
479 raise StackException, self.__base_file + ' already exists'
480
481 if (create_at!=False):
482 git.create_branch(self.__name, create_at)
483
484 os.makedirs(self.__patch_dir)
485
486 create_dirs(bases_dir)
487
488 self.create_empty_field('applied')
489 self.create_empty_field('unapplied')
490 self.create_empty_field('description')
491 os.makedirs(os.path.join(self._dir(), 'patches'))
492 os.makedirs(self.__refs_dir)
493 self.__begin_stack_check()
494
495 def convert(self):
496 """Either convert to use a separate patch directory, or
497 unconvert to place the patches in the same directory with
498 series control files
499 """
500 if self.__patch_dir == self._dir():
501 print 'Converting old-style to new-style...',
502 sys.stdout.flush()
503
504 self.__patch_dir = os.path.join(self._dir(), 'patches')
505 os.makedirs(self.__patch_dir)
506
507 for p in self.get_applied() + self.get_unapplied():
508 src = os.path.join(self._dir(), p)
509 dest = os.path.join(self.__patch_dir, p)
510 os.rename(src, dest)
511
512 print 'done'
513
514 else:
515 print 'Converting new-style to old-style...',
516 sys.stdout.flush()
517
518 for p in self.get_applied() + self.get_unapplied():
519 src = os.path.join(self.__patch_dir, p)
520 dest = os.path.join(self._dir(), p)
521 os.rename(src, dest)
522
523 if not os.listdir(self.__patch_dir):
524 os.rmdir(self.__patch_dir)
525 print 'done'
526 else:
527 print 'Patch directory %s is not empty.' % self.__name
528
529 self.__patch_dir = self._dir()
530
531 def rename(self, to_name):
532 """Renames a series
533 """
534 to_stack = Series(to_name)
535
536 if to_stack.is_initialised():
537 raise StackException, '"%s" already exists' % to_stack.get_branch()
538 if os.path.exists(to_stack.__base_file):
539 os.remove(to_stack.__base_file)
540
541 git.rename_branch(self.__name, to_name)
542
543 if os.path.isdir(self._dir()):
544 rename(os.path.join(self.__base_dir, 'patches'),
545 self.__name, to_stack.__name)
546 if os.path.exists(self.__base_file):
547 rename(os.path.join(self.__base_dir, 'refs', 'bases'),
548 self.__name, to_stack.__name)
549 if os.path.exists(self.__refs_dir):
550 rename(os.path.join(self.__base_dir, 'refs', 'patches'),
551 self.__name, to_stack.__name)
552
553 self.__init__(to_name)
554
555 def clone(self, target_series):
556 """Clones a series
557 """
558 try:
559 # allow cloning of branches not under StGIT control
560 base = read_string(self.get_base_file())
561 except:
562 base = git.get_head()
563 Series(target_series).init(create_at = base)
564 new_series = Series(target_series)
565
566 # generate an artificial description file
567 new_series.set_description('clone of "%s"' % self.__name)
568
569 # clone self's entire series as unapplied patches
570 try:
571 # allow cloning of branches not under StGIT control
572 applied = self.get_applied()
573 unapplied = self.get_unapplied()
574 patches = applied + unapplied
575 patches.reverse()
576 except:
577 patches = applied = unapplied = []
578 for p in patches:
579 patch = self.get_patch(p)
580 new_series.new_patch(p, message = patch.get_description(),
581 can_edit = False, unapplied = True,
582 bottom = patch.get_bottom(),
583 top = patch.get_top(),
584 author_name = patch.get_authname(),
585 author_email = patch.get_authemail(),
586 author_date = patch.get_authdate())
587
588 # fast forward the cloned series to self's top
589 new_series.forward_patches(applied)
590
591 def delete(self, force = False):
592 """Deletes an stgit series
593 """
594 if self.is_initialised():
595 patches = self.get_unapplied() + self.get_applied()
596 if not force and patches:
597 raise StackException, \
598 'Cannot delete: the series still contains patches'
599 for p in patches:
600 Patch(p, self.__patch_dir, self.__refs_dir).delete()
601
602 # remove the trash directory
603 for fname in os.listdir(self.__trash_dir):
604 os.remove(fname)
605 os.rmdir(self.__trash_dir)
606
607 # FIXME: find a way to get rid of those manual removals
608 # (move functionnality to StgitObject ?)
609 if os.path.exists(self.__applied_file):
610 os.remove(self.__applied_file)
611 if os.path.exists(self.__unapplied_file):
612 os.remove(self.__unapplied_file)
613 if os.path.exists(self.__hidden_file):
614 os.remove(self.__hidden_file)
615 if os.path.exists(self.__current_file):
616 os.remove(self.__current_file)
617 if os.path.exists(self.__descr_file):
618 os.remove(self.__descr_file)
619 if not os.listdir(self.__patch_dir):
620 os.rmdir(self.__patch_dir)
621 else:
622 print 'Patch directory %s is not empty.' % self.__name
623 if not os.listdir(self._dir()):
624 remove_dirs(os.path.join(self.__base_dir, 'patches'),
625 self.__name)
626 else:
627 print 'Series directory %s is not empty.' % self.__name
628 if not os.listdir(self.__refs_dir):
629 remove_dirs(os.path.join(self.__base_dir, 'refs', 'patches'),
630 self.__name)
631 else:
632 print 'Refs directory %s is not empty.' % self.__refs_dir
633
634 if os.path.exists(self.__base_file):
635 remove_file_and_dirs(
636 os.path.join(self.__base_dir, 'refs', 'bases'), self.__name)
637
638 def refresh_patch(self, files = None, message = None, edit = False,
639 show_patch = False,
640 cache_update = True,
641 author_name = None, author_email = None,
642 author_date = None,
643 committer_name = None, committer_email = None,
644 backup = False, sign_str = None, log = 'refresh'):
645 """Generates a new commit for the given patch
646 """
647 name = self.get_current()
648 if not name:
649 raise StackException, 'No patches applied'
650
651 patch = Patch(name, self.__patch_dir, self.__refs_dir)
652
653 descr = patch.get_description()
654 if not (message or descr):
655 edit = True
656 descr = ''
657 elif message:
658 descr = message
659
660 if not message and edit:
661 descr = edit_file(self, descr.rstrip(), \
662 'Please edit the description for patch "%s" ' \
663 'above.' % name, show_patch)
664
665 if not author_name:
666 author_name = patch.get_authname()
667 if not author_email:
668 author_email = patch.get_authemail()
669 if not author_date:
670 author_date = patch.get_authdate()
671 if not committer_name:
672 committer_name = patch.get_commname()
673 if not committer_email:
674 committer_email = patch.get_commemail()
675
676 if sign_str:
677 descr = '%s\n%s: %s <%s>\n' % (descr.rstrip(), sign_str,
678 committer_name, committer_email)
679
680 bottom = patch.get_bottom()
681
682 commit_id = git.commit(files = files,
683 message = descr, parents = [bottom],
684 cache_update = cache_update,
685 allowempty = True,
686 author_name = author_name,
687 author_email = author_email,
688 author_date = author_date,
689 committer_name = committer_name,
690 committer_email = committer_email)
691
692 patch.set_bottom(bottom, backup = backup)
693 patch.set_top(commit_id, backup = backup)
694 patch.set_description(descr)
695 patch.set_authname(author_name)
696 patch.set_authemail(author_email)
697 patch.set_authdate(author_date)
698 patch.set_commname(committer_name)
699 patch.set_commemail(committer_email)
700
701 if log:
702 self.log_patch(patch, log)
703
704 return commit_id
705
706 def undo_refresh(self):
707 """Undo the patch boundaries changes caused by 'refresh'
708 """
709 name = self.get_current()
710 assert(name)
711
712 patch = Patch(name, self.__patch_dir, self.__refs_dir)
713 old_bottom = patch.get_old_bottom()
714 old_top = patch.get_old_top()
715
716 # the bottom of the patch is not changed by refresh. If the
717 # old_bottom is different, there wasn't any previous 'refresh'
718 # command (probably only a 'push')
719 if old_bottom != patch.get_bottom() or old_top == patch.get_top():
720 raise StackException, 'No undo information available'
721
722 git.reset(tree_id = old_top, check_out = False)
723 if patch.restore_old_boundaries():
724 self.log_patch(patch, 'undo')
725
726 def new_patch(self, name, message = None, can_edit = True,
727 unapplied = False, show_patch = False,
728 top = None, bottom = None,
729 author_name = None, author_email = None, author_date = None,
730 committer_name = None, committer_email = None,
731 before_existing = False, refresh = True):
732 """Creates a new patch
733 """
734 if self.patch_applied(name) or self.patch_unapplied(name):
735 raise StackException, 'Patch "%s" already exists' % name
736
737 if not message and can_edit:
738 descr = edit_file(self, None, \
739 'Please enter the description for patch "%s" ' \
740 'above.' % name, show_patch)
741 else:
742 descr = message
743
744 head = git.get_head()
745
746 self.__begin_stack_check()
747
748 patch = Patch(name, self.__patch_dir, self.__refs_dir)
749 patch.create()
750
751 if bottom:
752 patch.set_bottom(bottom)
753 else:
754 patch.set_bottom(head)
755 if top:
756 patch.set_top(top)
757 else:
758 patch.set_top(head)
759
760 patch.set_description(descr)
761 patch.set_authname(author_name)
762 patch.set_authemail(author_email)
763 patch.set_authdate(author_date)
764 patch.set_commname(committer_name)
765 patch.set_commemail(committer_email)
766
767 if unapplied:
768 self.log_patch(patch, 'new')
769
770 patches = [patch.get_name()] + self.get_unapplied()
771
772 f = file(self.__unapplied_file, 'w+')
773 f.writelines([line + '\n' for line in patches])
774 f.close()
775 elif before_existing:
776 self.log_patch(patch, 'new')
777
778 insert_string(self.__applied_file, patch.get_name())
779 if not self.get_current():
780 self.__set_current(name)
781 else:
782 append_string(self.__applied_file, patch.get_name())
783 self.__set_current(name)
784 if refresh:
785 self.refresh_patch(cache_update = False, log = 'new')
786
787 def delete_patch(self, name):
788 """Deletes a patch
789 """
790 patch = Patch(name, self.__patch_dir, self.__refs_dir)
791
792 if self.__patch_is_current(patch):
793 self.pop_patch(name)
794 elif self.patch_applied(name):
795 raise StackException, 'Cannot remove an applied patch, "%s", ' \
796 'which is not current' % name
797 elif not name in self.get_unapplied():
798 raise StackException, 'Unknown patch "%s"' % name
799
800 # save the commit id to a trash file
801 write_string(os.path.join(self.__trash_dir, name), patch.get_top())
802
803 patch.delete()
804
805 unapplied = self.get_unapplied()
806 unapplied.remove(name)
807 f = file(self.__unapplied_file, 'w+')
808 f.writelines([line + '\n' for line in unapplied])
809 f.close()
810
811 if self.patch_hidden(name):
812 self.unhide_patch(name)
813
814 self.__begin_stack_check()
815
816 def forward_patches(self, names):
817 """Try to fast-forward an array of patches.
818
819 On return, patches in names[0:returned_value] have been pushed on the
820 stack. Apply the rest with push_patch
821 """
822 unapplied = self.get_unapplied()
823 self.__begin_stack_check()
824
825 forwarded = 0
826 top = git.get_head()
827
828 for name in names:
829 assert(name in unapplied)
830
831 patch = Patch(name, self.__patch_dir, self.__refs_dir)
832
833 head = top
834 bottom = patch.get_bottom()
835 top = patch.get_top()
836
837 # top != bottom always since we have a commit for each patch
838 if head == bottom:
839 # reset the backup information. No logging since the
840 # patch hasn't changed
841 patch.set_bottom(head, backup = True)
842 patch.set_top(top, backup = True)
843
844 else:
845 head_tree = git.get_commit(head).get_tree()
846 bottom_tree = git.get_commit(bottom).get_tree()
847 if head_tree == bottom_tree:
848 # We must just reparent this patch and create a new commit
849 # for it
850 descr = patch.get_description()
851 author_name = patch.get_authname()
852 author_email = patch.get_authemail()
853 author_date = patch.get_authdate()
854 committer_name = patch.get_commname()
855 committer_email = patch.get_commemail()
856
857 top_tree = git.get_commit(top).get_tree()
858
859 top = git.commit(message = descr, parents = [head],
860 cache_update = False,
861 tree_id = top_tree,
862 allowempty = True,
863 author_name = author_name,
864 author_email = author_email,
865 author_date = author_date,
866 committer_name = committer_name,
867 committer_email = committer_email)
868
869 patch.set_bottom(head, backup = True)
870 patch.set_top(top, backup = True)
871
872 self.log_patch(patch, 'push(f)')
873 else:
874 top = head
875 # stop the fast-forwarding, must do a real merge
876 break
877
878 forwarded+=1
879 unapplied.remove(name)
880
881 if forwarded == 0:
882 return 0
883
884 git.switch(top)
885
886 append_strings(self.__applied_file, names[0:forwarded])
887
888 f = file(self.__unapplied_file, 'w+')
889 f.writelines([line + '\n' for line in unapplied])
890 f.close()
891
892 self.__set_current(name)
893
894 return forwarded
895
896 def merged_patches(self, names):
897 """Test which patches were merged upstream by reverse-applying
898 them in reverse order. The function returns the list of
899 patches detected to have been applied. The state of the tree
900 is restored to the original one
901 """
902 patches = [Patch(name, self.__patch_dir, self.__refs_dir)
903 for name in names]
904 patches.reverse()
905
906 merged = []
907 for p in patches:
908 if git.apply_diff(p.get_top(), p.get_bottom()):
909 merged.append(p.get_name())
910 merged.reverse()
911
912 git.reset()
913
914 return merged
915
916 def push_patch(self, name, empty = False):
917 """Pushes a patch on the stack
918 """
919 unapplied = self.get_unapplied()
920 assert(name in unapplied)
921
922 self.__begin_stack_check()
923
924 patch = Patch(name, self.__patch_dir, self.__refs_dir)
925
926 head = git.get_head()
927 bottom = patch.get_bottom()
928 top = patch.get_top()
929
930 ex = None
931 modified = False
932
933 # top != bottom always since we have a commit for each patch
934 if empty:
935 # just make an empty patch (top = bottom = HEAD). This
936 # option is useful to allow undoing already merged
937 # patches. The top is updated by refresh_patch since we
938 # need an empty commit
939 patch.set_bottom(head, backup = True)
940 patch.set_top(head, backup = True)
941 modified = True
942 elif head == bottom:
943 # reset the backup information. No need for logging
944 patch.set_bottom(bottom, backup = True)
945 patch.set_top(top, backup = True)
946
947 git.switch(top)
948 else:
949 # new patch needs to be refreshed.
950 # The current patch is empty after merge.
951 patch.set_bottom(head, backup = True)
952 patch.set_top(head, backup = True)
953
954 # Try the fast applying first. If this fails, fall back to the
955 # three-way merge
956 if not git.apply_diff(bottom, top):
957 # if git.apply_diff() fails, the patch requires a diff3
958 # merge and can be reported as modified
959 modified = True
960
961 # merge can fail but the patch needs to be pushed
962 try:
963 git.merge(bottom, head, top, recursive = True)
964 except git.GitException, ex:
965 print >> sys.stderr, \
966 'The merge failed during "push". ' \
967 'Use "refresh" after fixing the conflicts'
968
969 append_string(self.__applied_file, name)
970
971 unapplied.remove(name)
972 f = file(self.__unapplied_file, 'w+')
973 f.writelines([line + '\n' for line in unapplied])
974 f.close()
975
976 self.__set_current(name)
977
978 # head == bottom case doesn't need to refresh the patch
979 if empty or head != bottom:
980 if not ex:
981 # if the merge was OK and no conflicts, just refresh the patch
982 # The GIT cache was already updated by the merge operation
983 if modified:
984 log = 'push(m)'
985 else:
986 log = 'push'
987 self.refresh_patch(cache_update = False, log = log)
988 else:
989 # we store the correctly merged files only for
990 # tracking the conflict history. Note that the
991 # git.merge() operations shouls always leave the index
992 # in a valid state (i.e. only stage 0 files)
993 self.refresh_patch(cache_update = False, log = 'push(c)')
994 raise StackException, str(ex)
995
996 return modified
997
998 def undo_push(self):
999 name = self.get_current()
1000 assert(name)
1001
1002 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1003 old_bottom = patch.get_old_bottom()
1004 old_top = patch.get_old_top()
1005
1006 # the top of the patch is changed by a push operation only
1007 # together with the bottom (otherwise the top was probably
1008 # modified by 'refresh'). If they are both unchanged, there
1009 # was a fast forward
1010 if old_bottom == patch.get_bottom() and old_top != patch.get_top():
1011 raise StackException, 'No undo information available'
1012
1013 git.reset()
1014 self.pop_patch(name)
1015 ret = patch.restore_old_boundaries()
1016 if ret:
1017 self.log_patch(patch, 'undo')
1018
1019 return ret
1020
1021 def pop_patch(self, name, keep = False):
1022 """Pops the top patch from the stack
1023 """
1024 applied = self.get_applied()
1025 applied.reverse()
1026 assert(name in applied)
1027
1028 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1029
1030 # only keep the local changes
1031 if keep and not git.apply_diff(git.get_head(), patch.get_bottom()):
1032 raise StackException, \
1033 'Failed to pop patches while preserving the local changes'
1034
1035 git.switch(patch.get_bottom(), keep)
1036
1037 # save the new applied list
1038 idx = applied.index(name) + 1
1039
1040 popped = applied[:idx]
1041 popped.reverse()
1042 unapplied = popped + self.get_unapplied()
1043
1044 f = file(self.__unapplied_file, 'w+')
1045 f.writelines([line + '\n' for line in unapplied])
1046 f.close()
1047
1048 del applied[:idx]
1049 applied.reverse()
1050
1051 f = file(self.__applied_file, 'w+')
1052 f.writelines([line + '\n' for line in applied])
1053 f.close()
1054
1055 if applied == []:
1056 self.__set_current(None)
1057 else:
1058 self.__set_current(applied[-1])
1059
1060 self.__end_stack_check()
1061
1062 def empty_patch(self, name):
1063 """Returns True if the patch is empty
1064 """
1065 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1066 bottom = patch.get_bottom()
1067 top = patch.get_top()
1068
1069 if bottom == top:
1070 return True
1071 elif git.get_commit(top).get_tree() \
1072 == git.get_commit(bottom).get_tree():
1073 return True
1074
1075 return False
1076
1077 def rename_patch(self, oldname, newname):
1078 applied = self.get_applied()
1079 unapplied = self.get_unapplied()
1080
1081 if oldname == newname:
1082 raise StackException, '"To" name and "from" name are the same'
1083
1084 if newname in applied or newname in unapplied:
1085 raise StackException, 'Patch "%s" already exists' % newname
1086
1087 if self.patch_hidden(oldname):
1088 self.unhide_patch(oldname)
1089 self.hide_patch(newname)
1090
1091 if oldname in unapplied:
1092 Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1093 unapplied[unapplied.index(oldname)] = newname
1094
1095 f = file(self.__unapplied_file, 'w+')
1096 f.writelines([line + '\n' for line in unapplied])
1097 f.close()
1098 elif oldname in applied:
1099 Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1100 if oldname == self.get_current():
1101 self.__set_current(newname)
1102
1103 applied[applied.index(oldname)] = newname
1104
1105 f = file(self.__applied_file, 'w+')
1106 f.writelines([line + '\n' for line in applied])
1107 f.close()
1108 else:
1109 raise StackException, 'Unknown patch "%s"' % oldname
1110
1111 def log_patch(self, patch, message):
1112 """Generate a log commit for a patch
1113 """
1114 top = git.get_commit(patch.get_top())
1115 msg = '%s\t%s' % (message, top.get_id_hash())
1116
1117 old_log = patch.get_log()
1118 if old_log:
1119 parents = [old_log]
1120 else:
1121 parents = []
1122
1123 log = git.commit(message = msg, parents = parents,
1124 cache_update = False, tree_id = top.get_tree(),
1125 allowempty = True)
1126 patch.set_log(log)
1127
1128 def hide_patch(self, name):
1129 """Add the patch to the hidden list.
1130 """
1131 if not self.patch_exists(name):
1132 raise StackException, 'Unknown patch "%s"' % name
1133 elif self.patch_hidden(name):
1134 raise StackException, 'Patch "%s" already hidden' % name
1135
1136 append_string(self.__hidden_file, name)
1137
1138 def unhide_patch(self, name):
1139 """Add the patch to the hidden list.
1140 """
1141 if not self.patch_exists(name):
1142 raise StackException, 'Unknown patch "%s"' % name
1143 hidden = self.get_hidden()
1144 if not name in hidden:
1145 raise StackException, 'Patch "%s" not hidden' % name
1146
1147 hidden.remove(name)
1148
1149 f = file(self.__hidden_file, 'w+')
1150 f.writelines([line + '\n' for line in hidden])
1151 f.close()