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