diff --git a/NEWS b/NEWS index d790b329..fac2ccce 100644 --- a/NEWS +++ b/NEWS @@ -1,7 +1,11 @@ 0.4: - allow 'up' inside a project directory (will automatically pull in all new - packages) + packages). (For past checkouts, you may need to put the project name into + $prjdir/.osc/_project yourself). - checkout: preserve mtimes +- add diff3 merge support. Locally modified files are merged with upstream changes + if possible, and go into Conflict state if that fails. +- add 'resolved' command to be used after manual merging. 0.3: diff --git a/README b/README index c5a9b298..20944c26 100644 --- a/README +++ b/README @@ -1,8 +1,5 @@ osc -- opensuse-commander with svn like handling ->>> BUG: at the moment, 'up' overwrites files with - local modifications. - Please send patches to poeml@suse.de, or work directly on https://forgesvn1.novell.com/svn/opensuse/trunk/buildservice/src/clientlib/python/osc/ diff --git a/TODO b/TODO index 7b0ac231..dc146afb 100644 --- a/TODO +++ b/TODO @@ -2,10 +2,8 @@ - implement 'mv' command - implement 'trigger-rebuild' -update: handle local modifications checkin: - - fix argument handling - handle error if PUT fails, so the change is not committed to localmeta diff --git a/osc/commandline.py b/osc/commandline.py index 9ba3a5eb..e0f2aa92 100755 --- a/osc/commandline.py +++ b/osc/commandline.py @@ -52,7 +52,8 @@ def main(): for p in pacs: if p.todo == []: for i in p.filenamelist: - if p.status(i) == 'M': + s = p.status(i) + if s == 'M' or s == 'C': p.todo.append(i) d = [] @@ -210,8 +211,12 @@ def main(): for p in pacs: - # save filelist before replacing the meta file + # save filelist and (modified) status before replacing the meta file saved_filenames = p.filenamelist + saved_modifiedfiles = [] + for i in p.filenamelist: + if p.status(i) == 'M': + saved_modifiedfiles.append(i) p.update_filesmeta() p = Package(p.dir) @@ -231,10 +236,13 @@ def main(): for filename in p.filenamelist: state = p.status(filename) - if state == 'M': - print 'file %s is locally modified... fixme' % filename + if state == 'M' and filename in saved_modifiedfiles: + print 'merging' + status_after_merge = p.mergefile(filename) + print statfrmt(status_after_merge, filename) + elif state == 'M': p.updatefile(filename) - print statfrmt('U', filename) + print statfrmt('M', filename) elif state == '!': p.updatefile(filename) print 'Restored \'%s\'' % filename @@ -247,7 +255,8 @@ def main(): p.update_pacmeta() - print ljust(p.name, 45), 'At revision %s.' % p.rev + #print ljust(p.name, 45), 'At revision %s.' % p.rev + print 'At revision %s.' % p.rev @@ -274,6 +283,20 @@ def main(): print statfrmt('D', filename) + elif cmd == 'resolved': + if len(sys.argv) < 3: + print '%s requires at least one argument' % cmd + sys.exit(1) + + args = parseargs() + pacs = findpacs(args) + + for p in pacs: + + for filename in p.todo: + print "Resolved conflicted state of '%s'" % filename + p.clear_from_conflictlist(filename) + elif cmd == 'id': print ''.join(get_user_id(sys.argv[2])) diff --git a/osc/core.py b/osc/core.py index 719428cb..21c0eb44 100755 --- a/osc/core.py +++ b/osc/core.py @@ -13,7 +13,6 @@ import urllib2 from urlparse import urlunsplit import cElementTree as ET from cStringIO import StringIO -from string import ljust from xml.dom.ext.reader import Sax2 @@ -102,6 +101,12 @@ class Package: self.filenamelist = [] self.filelist = [] for node in files_tree_root.findall('entry'): + try: + int(node.get('size')) + except: + print 'old _files metadata found.' + print 'run \'osc up\' after manually removing all "entry" lines from .osc/_files to upgrade.' + sys.exit(1) f = File(node.get('name'), node.get('md5'), int(node.get('size')), @@ -110,6 +115,7 @@ class Package: self.filenamelist.append(f.name) self.to_be_deleted = read_tobedeleted(self.dir) + self.in_conflict = read_inconflict(self.dir) self.todo = [] self.todo_send = [] @@ -139,12 +145,45 @@ class Package: if n not in self.to_be_deleted: self.to_be_deleted.append(n) + def put_on_conflictlist(self, n): + if n not in self.in_conflict: + self.in_conflict.append(n) + + def clear_from_conflictlist(self, n): + """delete an entry from the file, and remove the file if it would be empty""" + if n in self.in_conflict: + + filename = os.path.join(self.dir, n) + storefilename = os.path.join(self.storedir, n) + myfilename = os.path.join(self.dir, n + '.mine') + upfilename = os.path.join(self.dir, n + '.r' + self.rev) + + os.unlink(myfilename) + os.rename(upfilename, storefilename) + + self.in_conflict.remove(n) + + self.write_deletelist() + def write_deletelist(self): - fname = os.path.join(self.storedir, '_to_be_deleted') - f = open(fname, 'w') - f.write('\n'.join(self.to_be_deleted)) - f.write('\n') - f.close() + if len(self.to_be_deleted) == 0: + os.unlink(os.path.join(self.storedir, '_to_be_deleted')) + else: + fname = os.path.join(self.storedir, '_to_be_deleted') + f = open(fname, 'w') + f.write('\n'.join(self.to_be_deleted)) + f.write('\n') + f.close() + + def write_conflictlist(self): + if len(self.in_conflict) == 0: + os.unlink(os.path.join(self.storedir, '_in_conflict')) + else: + fname = os.path.join(self.storedir, '_in_conflict') + f = open(fname, 'w') + f.write('\n'.join(self.in_conflict)) + f.write('\n') + f.close() def updatefile(self, n): filename = os.path.join(self.dir, n) @@ -157,6 +196,31 @@ class Package: copy_file(filename, storefilename) os.utime(storefilename, (-1, mtime)) + def mergefile(self, n): + filename = os.path.join(self.dir, n) + storefilename = os.path.join(self.storedir, n) + myfilename = os.path.join(self.dir, n + '.mine') + upfilename = os.path.join(self.dir, n + '.r' + self.rev) + os.rename(filename, myfilename) + + get_source_file(self.prjname, self.name, n, targetfilename=upfilename) + + ret = os.system('cd %s; diff3 -m -E %s %s %s > %s' \ + % (self.dir, myfilename, storefilename, upfilename, filename)) + if ret == 0: + # merge was successful... clean up + os.rename(upfilename, filename) + copy_file(filename, storefilename) + os.unlink(myfilename) + return 'M' + else: + # unsuccessful merge + self.in_conflict.append(n) + self.write_conflictlist() + return 'C' + + + def update_filesmeta(self): meta = ''.join(show_files_meta(self.prjname, self.name)) f = open(os.path.join(self.storedir, '_files'), 'w') @@ -182,7 +246,8 @@ class Package: exists exists in _files x x - 'A' - x x x 'M', if digest differs, else ' ' + x x x ' ' if digest differs: 'M' + and if in conflicts file: 'C' x - - '?' x - x 'D' and listed in _to_be_deleted - x x '!' @@ -207,6 +272,8 @@ class Package: state = 'D' elif n in self.to_be_deleted: state = 'D' + elif n in self.in_conflict: + state = 'C' elif exists and exists_in_store and known_by_meta: #print self.findfilebyname(n) if dgst(os.path.join(self.dir, n)) != self.findfilebyname(n).md5: @@ -295,6 +362,18 @@ def read_tobedeleted(dir): return r +def read_inconflict(dir): + r = [] + fname = os.path.join(dir, store, '_in_conflict') + + if os.path.exists(fname): + + for i in open(fname, 'r').readlines(): + r.append(i.strip()) + + return r + + def parseargs(): if len(sys.argv) > 2: args = sys.argv[2:]