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