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