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