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