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