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