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