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