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