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