diff --git a/osc/commandline.py b/osc/commandline.py index bdda5fa0..450ea605 100755 --- a/osc/commandline.py +++ b/osc/commandline.py @@ -375,6 +375,12 @@ class Osc(cmdln.Cmdln): @cmdln.alias('di') + @cmdln.option('-r', '--revision', metavar='rev1[:rev2]', + help='If rev1 is specified it will compare your working copy against ' + 'the revision (rev1) on the server. ' + 'If rev1 and rev2 are specified it will compare rev1 against rev2' + '(changes in your working copy are ignored in this case).\n' + 'NOTE: if more than 1 package is specified --revision will be ignored!') def do_diff(self, subcmd, opts, *args): """${cmd_name}: Generates a diff @@ -389,28 +395,95 @@ class Osc(cmdln.Cmdln): args = parseargs(args) pacs = findpacs(args) - + difference_found = False - for p in pacs: - if p.todo == []: - for i in p.filenamelist: - s = p.status(i) - if s == 'M' or s == 'C': - p.todo.append(i) + d = [] + + rev1, rev2 = parseRevisionOption(opts.revision) + pac = pacs[0] + + if rev1 and rev2 and (len(pacs) == 1): + # this is currently not implemented + print >>sys.stderr, 'this feature isn\'t implemented yet' + sys.exit(1) + elif rev1 and (pac.rev != rev1) and (len(pacs) == 1): + # make a temp dir for checking out the project + import tempfile + tmpdir = tempfile.mkdtemp(rev1, pac.name, '/tmp') + curdir = os.getcwd() + os.chdir(tmpdir) + init_package_dir(conf.config['apiurl'], pac.prjname, pac.name, tmpdir, rev1) + os.chdir(curdir) + tmppac = Package(tmpdir) - d = [] - for filename in p.todo: - d.append('Index: %s\n' % filename) + changed_files = [] + added_files = [] + removed_files = [] + if pac.todo: + for file in pac.todo: + if file in tmppac.filenamelist: + if dgst(os.path.join(pac.dir, file)) != tmppac.findfilebyname(file).md5: + changed_files.append(file) + else: + added_files.append(file) + else: + changed_files, added_files, removed_files = pac.comparePac(tmppac) + + for file in changed_files: + tmppac.updatefile(file, rev1) + d.append('Index: %s\n' % file) d.append('===================================================================\n') - d.append(get_source_file_diff(p.dir, filename, p.rev)) - if d: - print ''.join(d) - difference_found = True + d.append(get_source_file_diff(pac.dir, file, rev1, file, tmppac.dir)) + tmppac.delete_localfile(file) + tmppac.delete_storefile(file) + + # this tempfile is used as a dummy file for difflib + (fd, filename) = tempfile.mkstemp(dir=tmppac.storedir) + + for file in added_files: + d.append('Index: %s\n' % file) + d.append('===================================================================\n') + d.append(get_source_file_diff(pac.dir, file, rev1, \ + os.path.basename(filename), \ + tmppac.storedir, file)) + + for file in removed_files: + tmppac.updatefile(file, rev1) + d.append('Index: %s\n' % file) + d.append('===================================================================\n') + d.append(get_source_file_diff(tmppac.storedir, \ + os.path.basename(filename), \ + rev1, file, tmppac.dir, file)) + tmppac.delete_localfile(file) + tmppac.delete_storefile(file) + + # clean up + os.unlink(filename) + for dir, dirnames, files in os.walk(tmppac.storedir): + for file in files: + os.unlink(os.path.join(dir, file)) + os.rmdir(tmppac.storedir) + os.rmdir(tmppac.dir) + else: + for p in pacs: + if p.todo == []: + for i in p.filenamelist: + s = p.status(i) + if s == 'M' or s == 'C': + p.todo.append(i) + + for filename in p.todo: + d.append('Index: %s\n' % filename) + d.append('===================================================================\n') + d.append(get_source_file_diff(p.dir, filename, p.rev)) + + + if d: + print ''.join(d) + difference_found = True if difference_found: return 1 - - def do_repourls(self, subcmd, opts, *args): """${cmd_name}: shows URLs of .repo files @@ -434,7 +507,9 @@ class Osc(cmdln.Cmdln): print url_tmpl % (p.prjname.replace(':', ':/'), platform, p.prjname) - + + @cmdln.option('-r', '--revision', metavar='rev', + help='checkout the specified revision') @cmdln.alias('co') def do_checkout(self, subcmd, opts, *args): """${cmd_name}: check out content from the repository @@ -461,11 +536,13 @@ class Osc(cmdln.Cmdln): except: pass + rev, dummy = parseRevisionOption(opts.revision) + if filename: - get_source_file(conf.config['apiurl'], project, package, filename) + get_source_file(conf.config['apiurl'], project, package, filename, revision=rev) elif package: - checkout_package(conf.config['apiurl'], project, package) + checkout_package(conf.config['apiurl'], project, package, rev) elif project: # all packages @@ -678,6 +755,10 @@ class Osc(cmdln.Cmdln): print + @cmdln.option('-r', '--revision', metavar='rev', + help='update to specified revision (this option will be ignored ' + 'if you are going to update the complete project or more than ' + 'one package)') @cmdln.alias('up') def do_update(self, subcmd, opts, *args): """${cmd_name}: Update a working copy @@ -719,6 +800,11 @@ class Osc(cmdln.Cmdln): pacs = findpacs(args) + if opts.revision and ( len(args) == 1): + rev, dummy = parseRevisionOption(opts.revision) + else: + rev = None + for p in pacs: if len(pacs) > 1: @@ -728,7 +814,7 @@ class Osc(cmdln.Cmdln): saved_modifiedfiles = [ f for f in p.filenamelist if p.status(f) == 'M' ] oldp = p - p.update_filesmeta() + p.update_filesmeta(rev) p = Package(p.dir) # which files do no longer exist upstream? @@ -754,13 +840,13 @@ class Osc(cmdln.Cmdln): status_after_merge = p.mergefile(filename) print statfrmt(status_after_merge, filename) elif state == 'M': - p.updatefile(filename) + p.updatefile(filename, rev) print statfrmt('U', filename) elif state == '!': - p.updatefile(filename) + p.updatefile(filename, rev) print 'Restored \'%s\'' % filename elif state == 'F': - p.updatefile(filename) + p.updatefile(filename, rev) print statfrmt('A', filename) elif state == ' ': pass diff --git a/osc/core.py b/osc/core.py index a9c85a4b..36e2d0a1 100755 --- a/osc/core.py +++ b/osc/core.py @@ -329,12 +329,12 @@ class Package: f.write('\n') f.close() - def updatefile(self, n): + def updatefile(self, n, revision): filename = os.path.join(self.dir, n) storefilename = os.path.join(self.storedir, n) mtime = self.findfilebyname(n).mtime - get_source_file(self.apiurl, self.prjname, self.name, n, targetfilename=filename) + get_source_file(self.apiurl, self.prjname, self.name, n, targetfilename=filename, revision=revision) os.utime(filename, (-1, mtime)) shutil.copy2(filename, storefilename) @@ -386,8 +386,8 @@ class Package: - def update_filesmeta(self): - meta = ''.join(show_files_meta(self.apiurl, self.prjname, self.name)) + def update_filesmeta(self, revision=None): + meta = ''.join(show_files_meta(self.apiurl, self.prjname, self.name, revision)) f = open(os.path.join(self.storedir, '_files'), 'w') f.write(meta) f.close() @@ -462,6 +462,30 @@ class Package: return state + def comparePac(self, pac): + """ + This method compares the local filelist with + the filelist of the passed package to see which files + were added, removed and changed. + """ + + changed_files = [] + added_files = [] + removed_files = [] + + for file in self.filenamelist: + if not file in self.to_be_deleted: + if file in pac.filenamelist: + if dgst(file) != pac.findfilebyname(file).md5: + changed_files.append(file) + else: + added_files.append(file) + + for file in pac.filenamelist: + if (not file in self.filenamelist) or (file in self.to_be_deleted): + removed_files.append(file) + + return changed_files, added_files, removed_files def merge(self, otherpac): self.todo += otherpac.todo @@ -733,7 +757,7 @@ def urlopen(url, data=None): return fd -def init_package_dir(apiurl, project, package, dir): +def init_package_dir(apiurl, project, package, dir, revision=None): if not os.path.isdir(store): os.mkdir(store) os.chdir(store) @@ -745,7 +769,7 @@ def init_package_dir(apiurl, project, package, dir): f.close f = open('_files', 'w') - f.write(''.join(show_files_meta(apiurl, project, package))) + f.write(''.join(show_files_meta(apiurl, project, package, revision))) f.close() f = open('_osclib_version', 'w') @@ -986,8 +1010,11 @@ def edit_user_meta(user, change_is_required=True): print 'Done.' -def show_files_meta(apiurl, prj, pac): - f = http_GET(makeurl(apiurl, ['source', prj, pac])) +def show_files_meta(apiurl, prj, pac, revision=None): + query = [] + if revision: + query.append('rev=%s' % revision) + f = http_GET(makeurl(apiurl, ['source', prj, pac], query=query)) return f.readlines() @@ -1043,8 +1070,13 @@ def get_user_meta(apiurl, user): return None -def get_source_file(apiurl, prj, package, filename, targetfilename=None): - u = makeurl(apiurl, ['source', prj, package, pathname2url(filename)]) +def get_source_file(apiurl, prj, package, filename, targetfilename=None, revision = None): + query = [] + if revision: + query.append('rev=%s' % quote_plus(revision)) + + u = makeurl(apiurl, ['source', prj, package, pathname2url(filename)], query=query) + # print 'url: %s' % u f = http_GET(u) o = open(targetfilename or filename, 'w') @@ -1082,10 +1114,26 @@ def binary_file(fn): return binary(open(fn, 'r').read(4096)) -def get_source_file_diff(dir, filename, rev): +def get_source_file_diff(dir, filename, rev, oldfilename = None, olddir = None, origfilename = None): + """ + This methods diffs oldfilename against filename (so filename will + be shown as the new file). + The variable origfilename is used if filename and oldfilename differ + in their names (for instance if a tempfile is used for filename etc.) + """ + import difflib - file1 = os.path.join(dir, store, filename) # stored original + if not oldfilename: + oldfilename = filename + + if not olddir: + olddir = os.path.join(dir, store) + + if not origfilename: + origfilename = filename + + file1 = os.path.join(olddir, oldfilename) # old/stored original file2 = os.path.join(dir, filename) # working copy f1 = open(file1, 'r') @@ -1097,14 +1145,14 @@ def get_source_file_diff(dir, filename, rev): f2.close() if binary(s1) or binary (s2): - d = ['Binary file %s has changed\n' % filename] + d = ['Binary file %s has changed\n' % origfilename] else: d = difflib.unified_diff(\ s1.splitlines(1), \ s2.splitlines(1), \ - fromfile = '%s (revision %s)' % (filename, rev), \ - tofile = '%s (working copy)' % filename) + fromfile = '%s (revision %s)' % (origfilename, rev), \ + tofile = '%s (working copy)' % origfilename) # if file doesn't end with newline, we need to append one in the diff result d = list(d) @@ -1136,15 +1184,15 @@ def make_dir(apiurl, project, package): return(os.path.join(project, package)) -def checkout_package(apiurl, project, package): +def checkout_package(apiurl, project, package, revision=None): olddir = os.getcwd() os.chdir(make_dir(apiurl, project, package)) - init_package_dir(apiurl, project, package, store) + init_package_dir(apiurl, project, package, store, revision) p = Package(os.curdir) for filename in p.filenamelist: - p.updatefile(filename) + p.updatefile(filename, revision) print 'A ', os.path.join(project, package, filename) os.chdir(olddir) @@ -1535,3 +1583,27 @@ def wipebinaries(apiurl, project, package=None, arch=None, repo=None): sys.exit(1) root = ET.parse(f).getroot() return root.get('code') + +def parseRevisionOption(string): + """ + retrun a tuple which contains the revisions + """ + + if string: + if ':' in string: + splitted_rev = string.split(':') + try: + for i in splitted_rev: + int(i) + return splitted_rev + except ValueError: + print >>sys.stderr, 'your revision \'%s\' will be ignored' % string + return None, None + else: + if string.isdigit(): + return string, None + else: + print >>sys.stderr, 'your revision \'%s\' will be ignored' % string + return None, None + else: + return None, None