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