#! /usr/bin/env python | |
"""Remote CVS -- command line interface""" | |
# XXX To do: | |
# | |
# Bugs: | |
# - if the remote file is deleted, "rcvs update" will fail | |
# | |
# Functionality: | |
# - cvs rm | |
# - descend into directories (alraedy done for update) | |
# - conflict resolution | |
# - other relevant commands? | |
# - branches | |
# | |
# - Finesses: | |
# - retain file mode's x bits | |
# - complain when "nothing known about filename" | |
# - edit log message the way CVS lets you edit it | |
# - cvs diff -rREVA -rREVB | |
# - send mail the way CVS sends it | |
# | |
# Performance: | |
# - cache remote checksums (for every revision ever seen!) | |
# - translate symbolic revisions to numeric revisions | |
# | |
# Reliability: | |
# - remote locking | |
# | |
# Security: | |
# - Authenticated RPC? | |
from cvslib import CVS, File | |
import md5 | |
import os | |
import string | |
import sys | |
from cmdfw import CommandFrameWork | |
DEF_LOCAL = 1 # Default -l | |
class MyFile(File): | |
def action(self): | |
"""Return a code indicating the update status of this file. | |
The possible return values are: | |
'=' -- everything's fine | |
'0' -- file doesn't exist anywhere | |
'?' -- exists locally only | |
'A' -- new locally | |
'R' -- deleted locally | |
'U' -- changed remotely, no changes locally | |
(includes new remotely or deleted remotely) | |
'M' -- changed locally, no changes remotely | |
'C' -- conflict: changed locally as well as remotely | |
(includes cases where the file has been added | |
or removed locally and remotely) | |
'D' -- deleted remotely | |
'N' -- new remotely | |
'r' -- get rid of entry | |
'c' -- create entry | |
'u' -- update entry | |
(and probably others :-) | |
""" | |
if not self.lseen: | |
self.getlocal() | |
if not self.rseen: | |
self.getremote() | |
if not self.eseen: | |
if not self.lsum: | |
if not self.rsum: return '0' # Never heard of | |
else: | |
return 'N' # New remotely | |
else: # self.lsum | |
if not self.rsum: return '?' # Local only | |
# Local and remote, but no entry | |
if self.lsum == self.rsum: | |
return 'c' # Restore entry only | |
else: return 'C' # Real conflict | |
else: # self.eseen | |
if not self.lsum: | |
if self.edeleted: | |
if self.rsum: return 'R' # Removed | |
else: return 'r' # Get rid of entry | |
else: # not self.edeleted | |
if self.rsum: | |
print "warning:", | |
print self.file, | |
print "was lost" | |
return 'U' | |
else: return 'r' # Get rid of entry | |
else: # self.lsum | |
if not self.rsum: | |
if self.enew: return 'A' # New locally | |
else: return 'D' # Deleted remotely | |
else: # self.rsum | |
if self.enew: | |
if self.lsum == self.rsum: | |
return 'u' | |
else: | |
return 'C' | |
if self.lsum == self.esum: | |
if self.esum == self.rsum: | |
return '=' | |
else: | |
return 'U' | |
elif self.esum == self.rsum: | |
return 'M' | |
elif self.lsum == self.rsum: | |
return 'u' | |
else: | |
return 'C' | |
def update(self): | |
code = self.action() | |
if code == '=': return | |
print code, self.file | |
if code in ('U', 'N'): | |
self.get() | |
elif code == 'C': | |
print "%s: conflict resolution not yet implemented" % \ | |
self.file | |
elif code == 'D': | |
remove(self.file) | |
self.eseen = 0 | |
elif code == 'r': | |
self.eseen = 0 | |
elif code in ('c', 'u'): | |
self.eseen = 1 | |
self.erev = self.rrev | |
self.enew = 0 | |
self.edeleted = 0 | |
self.esum = self.rsum | |
self.emtime, self.ectime = os.stat(self.file)[-2:] | |
self.extra = '' | |
def commit(self, message = ""): | |
code = self.action() | |
if code in ('A', 'M'): | |
self.put(message) | |
return 1 | |
elif code == 'R': | |
print "%s: committing removes not yet implemented" % \ | |
self.file | |
elif code == 'C': | |
print "%s: conflict resolution not yet implemented" % \ | |
self.file | |
def diff(self, opts = []): | |
self.action() # To update lseen, rseen | |
flags = '' | |
rev = self.rrev | |
# XXX should support two rev options too! | |
for o, a in opts: | |
if o == '-r': | |
rev = a | |
else: | |
flags = flags + ' ' + o + a | |
if rev == self.rrev and self.lsum == self.rsum: | |
return | |
flags = flags[1:] | |
fn = self.file | |
data = self.proxy.get((fn, rev)) | |
sum = md5.new(data).digest() | |
if self.lsum == sum: | |
return | |
import tempfile | |
tf = tempfile.NamedTemporaryFile() | |
tf.write(data) | |
tf.flush() | |
print 'diff %s -r%s %s' % (flags, rev, fn) | |
sts = os.system('diff %s %s %s' % (flags, tf.name, fn)) | |
if sts: | |
print '='*70 | |
def commitcheck(self): | |
return self.action() != 'C' | |
def put(self, message = ""): | |
print "Checking in", self.file, "..." | |
data = open(self.file).read() | |
if not self.enew: | |
self.proxy.lock(self.file) | |
messages = self.proxy.put(self.file, data, message) | |
if messages: | |
print messages | |
self.setentry(self.proxy.head(self.file), self.lsum) | |
def get(self): | |
data = self.proxy.get(self.file) | |
f = open(self.file, 'w') | |
f.write(data) | |
f.close() | |
self.setentry(self.rrev, self.rsum) | |
def log(self, otherflags): | |
print self.proxy.log(self.file, otherflags) | |
def add(self): | |
self.eseen = 0 # While we're hacking... | |
self.esum = self.lsum | |
self.emtime, self.ectime = 0, 0 | |
self.erev = '' | |
self.enew = 1 | |
self.edeleted = 0 | |
self.eseen = 1 # Done | |
self.extra = '' | |
def setentry(self, erev, esum): | |
self.eseen = 0 # While we're hacking... | |
self.esum = esum | |
self.emtime, self.ectime = os.stat(self.file)[-2:] | |
self.erev = erev | |
self.enew = 0 | |
self.edeleted = 0 | |
self.eseen = 1 # Done | |
self.extra = '' | |
SENDMAIL = "/usr/lib/sendmail -t" | |
MAILFORM = """To: %s | |
Subject: CVS changes: %s | |
...Message from rcvs... | |
Committed files: | |
%s | |
Log message: | |
%s | |
""" | |
class RCVS(CVS): | |
FileClass = MyFile | |
def __init__(self): | |
CVS.__init__(self) | |
def update(self, files): | |
for e in self.whichentries(files, 1): | |
e.update() | |
def commit(self, files, message = ""): | |
list = self.whichentries(files) | |
if not list: return | |
ok = 1 | |
for e in list: | |
if not e.commitcheck(): | |
ok = 0 | |
if not ok: | |
print "correct above errors first" | |
return | |
if not message: | |
message = raw_input("One-liner: ") | |
committed = [] | |
for e in list: | |
if e.commit(message): | |
committed.append(e.file) | |
self.mailinfo(committed, message) | |
def mailinfo(self, files, message = ""): | |
towhom = "sjoerd@cwi.nl, jack@cwi.nl" # XXX | |
mailtext = MAILFORM % (towhom, string.join(files), | |
string.join(files), message) | |
print '-'*70 | |
print mailtext | |
print '-'*70 | |
ok = raw_input("OK to mail to %s? " % towhom) | |
if string.lower(string.strip(ok)) in ('y', 'ye', 'yes'): | |
p = os.popen(SENDMAIL, "w") | |
p.write(mailtext) | |
sts = p.close() | |
if sts: | |
print "Sendmail exit status %s" % str(sts) | |
else: | |
print "Mail sent." | |
else: | |
print "No mail sent." | |
def report(self, files): | |
for e in self.whichentries(files): | |
e.report() | |
def diff(self, files, opts): | |
for e in self.whichentries(files): | |
e.diff(opts) | |
def add(self, files): | |
if not files: | |
raise RuntimeError, "'cvs add' needs at least one file" | |
list = [] | |
for e in self.whichentries(files, 1): | |
e.add() | |
def rm(self, files): | |
if not files: | |
raise RuntimeError, "'cvs rm' needs at least one file" | |
raise RuntimeError, "'cvs rm' not yet imlemented" | |
def log(self, files, opts): | |
flags = '' | |
for o, a in opts: | |
flags = flags + ' ' + o + a | |
for e in self.whichentries(files): | |
e.log(flags) | |
def whichentries(self, files, localfilestoo = 0): | |
if files: | |
list = [] | |
for file in files: | |
if self.entries.has_key(file): | |
e = self.entries[file] | |
else: | |
e = self.FileClass(file) | |
self.entries[file] = e | |
list.append(e) | |
else: | |
list = self.entries.values() | |
for file in self.proxy.listfiles(): | |
if self.entries.has_key(file): | |
continue | |
e = self.FileClass(file) | |
self.entries[file] = e | |
list.append(e) | |
if localfilestoo: | |
for file in os.listdir(os.curdir): | |
if not self.entries.has_key(file) \ | |
and not self.ignored(file): | |
e = self.FileClass(file) | |
self.entries[file] = e | |
list.append(e) | |
list.sort() | |
if self.proxy: | |
for e in list: | |
if e.proxy is None: | |
e.proxy = self.proxy | |
return list | |
class rcvs(CommandFrameWork): | |
GlobalFlags = 'd:h:p:qvL' | |
UsageMessage = \ | |
"usage: rcvs [-d directory] [-h host] [-p port] [-q] [-v] [subcommand arg ...]" | |
PostUsageMessage = \ | |
"If no subcommand is given, the status of all files is listed" | |
def __init__(self): | |
"""Constructor.""" | |
CommandFrameWork.__init__(self) | |
self.proxy = None | |
self.cvs = RCVS() | |
def close(self): | |
if self.proxy: | |
self.proxy._close() | |
self.proxy = None | |
def recurse(self): | |
self.close() | |
names = os.listdir(os.curdir) | |
for name in names: | |
if name == os.curdir or name == os.pardir: | |
continue | |
if name == "CVS": | |
continue | |
if not os.path.isdir(name): | |
continue | |
if os.path.islink(name): | |
continue | |
print "--- entering subdirectory", name, "---" | |
os.chdir(name) | |
try: | |
if os.path.isdir("CVS"): | |
self.__class__().run() | |
else: | |
self.recurse() | |
finally: | |
os.chdir(os.pardir) | |
print "--- left subdirectory", name, "---" | |
def options(self, opts): | |
self.opts = opts | |
def ready(self): | |
import rcsclient | |
self.proxy = rcsclient.openrcsclient(self.opts) | |
self.cvs.setproxy(self.proxy) | |
self.cvs.getentries() | |
def default(self): | |
self.cvs.report([]) | |
def do_report(self, opts, files): | |
self.cvs.report(files) | |
def do_update(self, opts, files): | |
"""update [-l] [-R] [file] ...""" | |
local = DEF_LOCAL | |
for o, a in opts: | |
if o == '-l': local = 1 | |
if o == '-R': local = 0 | |
self.cvs.update(files) | |
self.cvs.putentries() | |
if not local and not files: | |
self.recurse() | |
flags_update = '-lR' | |
do_up = do_update | |
flags_up = flags_update | |
def do_commit(self, opts, files): | |
"""commit [-m message] [file] ...""" | |
message = "" | |
for o, a in opts: | |
if o == '-m': message = a | |
self.cvs.commit(files, message) | |
self.cvs.putentries() | |
flags_commit = 'm:' | |
do_com = do_commit | |
flags_com = flags_commit | |
def do_diff(self, opts, files): | |
"""diff [difflags] [file] ...""" | |
self.cvs.diff(files, opts) | |
flags_diff = 'cbitwcefhnlr:sD:S:' | |
do_dif = do_diff | |
flags_dif = flags_diff | |
def do_add(self, opts, files): | |
"""add file ...""" | |
if not files: | |
print "'rcvs add' requires at least one file" | |
return | |
self.cvs.add(files) | |
self.cvs.putentries() | |
def do_remove(self, opts, files): | |
"""remove file ...""" | |
if not files: | |
print "'rcvs remove' requires at least one file" | |
return | |
self.cvs.remove(files) | |
self.cvs.putentries() | |
do_rm = do_remove | |
def do_log(self, opts, files): | |
"""log [rlog-options] [file] ...""" | |
self.cvs.log(files, opts) | |
flags_log = 'bhLNRtd:s:V:r:' | |
def remove(fn): | |
try: | |
os.unlink(fn) | |
except os.error: | |
pass | |
def main(): | |
r = rcvs() | |
try: | |
r.run() | |
finally: | |
r.close() | |
if __name__ == "__main__": | |
main() |