Merge branch 'master' of github.com:CouchPotato/CouchPotato
Conflicts: couchpotato/core/plugins/searcher/__init__.py
This commit is contained in:
+2
-1
@@ -1,3 +1,4 @@
|
|||||||
/settings.conf
|
/settings.conf
|
||||||
/logs/*.log
|
/logs/*.log
|
||||||
/_source/
|
/_source/
|
||||||
|
/_data/
|
||||||
@@ -23,6 +23,8 @@ def cmd_couchpotato(base_path, args):
|
|||||||
dest = 'quiet', help = "Don't log to console")
|
dest = 'quiet', help = "Don't log to console")
|
||||||
parser.add_argument('-d', '--daemon', action = 'store_true',
|
parser.add_argument('-d', '--daemon', action = 'store_true',
|
||||||
dest = 'daemonize', help = 'Daemonize the app')
|
dest = 'daemonize', help = 'Daemonize the app')
|
||||||
|
parser.add_argument('-g', '--nogit', action = 'store_true',
|
||||||
|
dest = 'git', help = 'Running from git')
|
||||||
|
|
||||||
options = parser.parse_args(args)
|
options = parser.parse_args(args)
|
||||||
|
|
||||||
@@ -46,6 +48,7 @@ def cmd_couchpotato(base_path, args):
|
|||||||
# Register environment settings
|
# Register environment settings
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
Env.get('settings').setFile(os.path.join(options.data_dir, 'settings.conf'))
|
Env.get('settings').setFile(os.path.join(options.data_dir, 'settings.conf'))
|
||||||
|
Env.set('uses_git', not options.git)
|
||||||
Env.set('app_dir', base_path)
|
Env.set('app_dir', base_path)
|
||||||
Env.set('data_dir', options.data_dir)
|
Env.set('data_dir', options.data_dir)
|
||||||
Env.set('log_path', os.path.join(log_dir, 'CouchPotato.log'))
|
Env.set('log_path', os.path.join(log_dir, 'CouchPotato.log'))
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ class Blackhole(Downloader):
|
|||||||
log.debug('Failed download file: %s' % data.get('name'))
|
log.debug('Failed download file: %s' % data.get('name'))
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
|
log.info('Downloading: %s' % data.get('url'))
|
||||||
file = urllib.urlopen(data.get('url')).read()
|
file = urllib.urlopen(data.get('url')).read()
|
||||||
|
|
||||||
with open(fullPath, 'wb') as f:
|
with open(fullPath, 'wb') as f:
|
||||||
|
|||||||
@@ -1,20 +1,47 @@
|
|||||||
|
from couchpotato.api import addApiView
|
||||||
from couchpotato.core.event import addEvent
|
from couchpotato.core.event import addEvent
|
||||||
from couchpotato.core.helpers.request import jsonified
|
from couchpotato.core.helpers.request import jsonified
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Notification(Plugin):
|
class Notification(Plugin):
|
||||||
|
|
||||||
default_title = 'CouchPotato'
|
default_title = 'CouchPotato'
|
||||||
test_message = 'ZOMG Lazors Pewpewpew!'
|
test_message = 'ZOMG Lazors Pewpewpew!'
|
||||||
|
|
||||||
|
listen_to = []
|
||||||
|
dont_listen_to = []
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
addEvent('notify', self.notify)
|
addEvent('notify', self.notify)
|
||||||
|
addEvent('notify.%s' % self.getName().lower(), self.notify)
|
||||||
|
|
||||||
def notify(self, message = '', data = {}):
|
addApiView(self.testNotifyName(), self.test)
|
||||||
|
|
||||||
|
def notify(self, message = '', data = {}, type = ''):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def test(self):
|
def test(self):
|
||||||
success = self.notify(message = self.test_message)
|
|
||||||
|
|
||||||
return jsonified({'success': success})
|
test_type = self.testNotifyName()
|
||||||
|
|
||||||
|
log.info('Sending test to %s' % test_type)
|
||||||
|
|
||||||
|
success = self.notify(
|
||||||
|
message = self.test_message,
|
||||||
|
data = {},
|
||||||
|
type = test_type
|
||||||
|
)
|
||||||
|
|
||||||
|
#return jsonified({'success': success})
|
||||||
|
|
||||||
|
def dontNotify(self, type = ''):
|
||||||
|
return (not type in self.listen_to and len(self.listen_to) == 0 and type != self.testNotifyName()) \
|
||||||
|
or type in self.dont_listen_to \
|
||||||
|
or self.isDisabled()
|
||||||
|
|
||||||
|
def testNotifyName(self):
|
||||||
|
return 'notify.%s.test' % self.getName().lower()
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ from couchpotato.api import addApiView
|
|||||||
from couchpotato.core.event import addEvent, fireEvent
|
from couchpotato.core.event import addEvent, fireEvent
|
||||||
from couchpotato.core.helpers.request import jsonified
|
from couchpotato.core.helpers.request import jsonified
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.notifications.base import Notification
|
||||||
import time
|
import time
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
class CoreNotifier(Plugin):
|
class CoreNotifier(Notification):
|
||||||
|
|
||||||
messages = []
|
messages = []
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ class CoreNotifier(Plugin):
|
|||||||
|
|
||||||
self.registerStatic(__file__)
|
self.registerStatic(__file__)
|
||||||
|
|
||||||
def notify(self, message = '', data = {}):
|
def notify(self, message = '', data = {}, type = None):
|
||||||
self.add(data = {
|
self.add(data = {
|
||||||
'message': message,
|
'message': message,
|
||||||
'raw': data,
|
'raw': data,
|
||||||
|
|||||||
@@ -12,19 +12,13 @@ log = CPLog(__name__)
|
|||||||
|
|
||||||
class Growl(Notification):
|
class Growl(Notification):
|
||||||
|
|
||||||
def __init__(self):
|
listen_to = ['movie.downloaded', 'movie.snatched']
|
||||||
addEvent('notify', self.notify)
|
|
||||||
addEvent('notify.growl', self.notify)
|
|
||||||
|
|
||||||
addApiView('notify.growl.test', self.test)
|
|
||||||
|
|
||||||
def conf(self, attr):
|
def conf(self, attr):
|
||||||
return Env.setting(attr, 'growl')
|
return Env.setting(attr, 'growl')
|
||||||
|
|
||||||
def notify(self, message = '', data = {}):
|
def notify(self, message = '', data = {}, type = None):
|
||||||
|
if self.dontNotify(type): return
|
||||||
if self.isDisabled():
|
|
||||||
return
|
|
||||||
|
|
||||||
hosts = [x.strip() for x in self.conf('host').split(",")]
|
hosts = [x.strip() for x in self.conf('host').split(",")]
|
||||||
password = self.conf('password')
|
password = self.conf('password')
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
from .main import History
|
||||||
|
|
||||||
|
def start():
|
||||||
|
return History()
|
||||||
|
|
||||||
|
config = []
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
from couchpotato import get_session
|
||||||
|
from couchpotato.core.event import addEvent
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.notifications.base import Notification
|
||||||
|
from couchpotato.core.settings.model import History as Hist
|
||||||
|
import time
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class History(Notification):
|
||||||
|
|
||||||
|
listen_to = ['movie.downloaded', 'movie.snatched', 'movie.renaming.']
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
|
||||||
|
addEvent('notify', self.notify)
|
||||||
|
|
||||||
|
addEvent('app.load', self.test)
|
||||||
|
|
||||||
|
|
||||||
|
def notify(self, message = '', data = {}, type = None):
|
||||||
|
if self.dontNotify(type): return
|
||||||
|
|
||||||
|
db = get_session()
|
||||||
|
history = Hist(
|
||||||
|
added = int(time.time()),
|
||||||
|
message = message,
|
||||||
|
type = type,
|
||||||
|
release_id = data.get('id', 0)
|
||||||
|
)
|
||||||
|
db.add(history)
|
||||||
|
db.commit()
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
from couchpotato.api import addApiView
|
from couchpotato.api import addApiView
|
||||||
from couchpotato.core.event import addEvent
|
|
||||||
from couchpotato.core.helpers.request import getParams, jsonified
|
from couchpotato.core.helpers.request import getParams, jsonified
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.notifications.base import Notification
|
from couchpotato.core.notifications.base import Notification
|
||||||
@@ -19,11 +18,11 @@ log = CPLog(__name__)
|
|||||||
|
|
||||||
class NMJ(Notification):
|
class NMJ(Notification):
|
||||||
|
|
||||||
def __init__(self):
|
listen_to = ['movie.downloaded', 'movie.snatched']
|
||||||
addEvent('notify', self.notify)
|
|
||||||
addEvent('notify.nmj', self.notify)
|
def __init__(self):
|
||||||
|
super(NMJ, self).__init__()
|
||||||
|
|
||||||
addApiView('notify.nmj.test', self.test)
|
|
||||||
addApiView('notify.nmj.auto_config', self.autoConfig)
|
addApiView('notify.nmj.auto_config', self.autoConfig)
|
||||||
|
|
||||||
def conf(self, attr):
|
def conf(self, attr):
|
||||||
@@ -76,10 +75,8 @@ class NMJ(Notification):
|
|||||||
'mount': mount,
|
'mount': mount,
|
||||||
})
|
})
|
||||||
|
|
||||||
def notify(self, message = '', data = {}):
|
def notify(self, message = '', data = {}, type = None):
|
||||||
|
if self.dontNotify(type): return
|
||||||
if self.isDisabled():
|
|
||||||
return False
|
|
||||||
|
|
||||||
host = self.conf('host')
|
host = self.conf('host')
|
||||||
mount = self.conf('mount')
|
mount = self.conf('mount')
|
||||||
|
|||||||
@@ -16,19 +16,13 @@ class Notifo(Notification):
|
|||||||
|
|
||||||
url = 'https://api.notifo.com/v1/send_notification'
|
url = 'https://api.notifo.com/v1/send_notification'
|
||||||
|
|
||||||
def __init__(self):
|
listen_to = ['movie.downloaded', 'movie.snatched']
|
||||||
addEvent('notify', self.notify)
|
|
||||||
addEvent('notify.notifo', self.notify)
|
|
||||||
|
|
||||||
addApiView('notify.notifo.test', self.test)
|
|
||||||
|
|
||||||
def conf(self, attr):
|
def conf(self, attr):
|
||||||
return Env.setting(attr, 'notifo')
|
return Env.setting(attr, 'notifo')
|
||||||
|
|
||||||
def notify(self, message = '', data = {}):
|
def notify(self, message = '', data = {}, type = None):
|
||||||
|
if self.dontNotify(type): return
|
||||||
if self.isDisabled():
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = urllib.urlencode({
|
data = urllib.urlencode({
|
||||||
|
|||||||
@@ -10,16 +10,10 @@ log = CPLog(__name__)
|
|||||||
|
|
||||||
class Plex(Notification):
|
class Plex(Notification):
|
||||||
|
|
||||||
def __init__(self):
|
listen_to = ['movie.downloaded', 'movie.snatched']
|
||||||
addEvent('notify', self.notify)
|
|
||||||
addEvent('notify.plex', self.notify)
|
|
||||||
|
|
||||||
addApiView('notify.plex.test', self.test)
|
def notify(self, message = '', data = {}, type = None):
|
||||||
|
if self.dontNotify(type): return
|
||||||
def notify(self, message = '', data = {}):
|
|
||||||
|
|
||||||
if self.isDisabled():
|
|
||||||
return
|
|
||||||
|
|
||||||
log.info('Sending notification to Plex')
|
log.info('Sending notification to Plex')
|
||||||
hosts = [x.strip() for x in self.conf('host').split(",")]
|
hosts = [x.strip() for x in self.conf('host').split(",")]
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
from couchpotato.api import addApiView
|
|
||||||
from couchpotato.core.event import addEvent
|
|
||||||
from couchpotato.core.helpers.encoding import toUnicode
|
from couchpotato.core.helpers.encoding import toUnicode
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.notifications.base import Notification
|
from couchpotato.core.notifications.base import Notification
|
||||||
@@ -11,16 +9,10 @@ log = CPLog(__name__)
|
|||||||
|
|
||||||
class Prowl(Notification):
|
class Prowl(Notification):
|
||||||
|
|
||||||
def __init__(self):
|
listen_to = ['movie.downloaded', 'movie.snatched']
|
||||||
addEvent('notify', self.notify)
|
|
||||||
addEvent('notify.prowl', self.notify)
|
|
||||||
|
|
||||||
addApiView('notify.prowl.test', self.test)
|
def notify(self, message = '', data = {}, type = None):
|
||||||
|
if self.dontNotify(type): return
|
||||||
def notify(self, message = '', data = {}):
|
|
||||||
|
|
||||||
if self.isDisabled():
|
|
||||||
return
|
|
||||||
|
|
||||||
http_handler = HTTPSConnection('api.prowlapp.com')
|
http_handler = HTTPSConnection('api.prowlapp.com')
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
from couchpotato.api import addApiView
|
|
||||||
from couchpotato.core.event import addEvent
|
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.notifications.base import Notification
|
from couchpotato.core.notifications.base import Notification
|
||||||
import base64
|
import base64
|
||||||
@@ -11,16 +9,10 @@ log = CPLog(__name__)
|
|||||||
|
|
||||||
class XBMC(Notification):
|
class XBMC(Notification):
|
||||||
|
|
||||||
def __init__(self):
|
listen_to = ['movie.downloaded', 'movie.snatched']
|
||||||
addEvent('notify', self.notify)
|
|
||||||
addEvent('notify.xbmc', self.notify)
|
|
||||||
|
|
||||||
addApiView('notify.xbmc.test', self.test)
|
def notify(self, message = '', data = {}, type = None):
|
||||||
|
if self.dontNotify(type): return
|
||||||
def notify(self, message = '', data = {}):
|
|
||||||
|
|
||||||
if self.isDisabled():
|
|
||||||
return
|
|
||||||
|
|
||||||
for host in [x.strip() for x in self.conf('host').split(",")]:
|
for host in [x.strip() for x in self.conf('host').split(",")]:
|
||||||
self.send({'command': 'ExecBuiltIn', 'parameter': 'Notification(CouchPotato, %s)' % message}, host)
|
self.send({'command': 'ExecBuiltIn', 'parameter': 'Notification(CouchPotato, %s)' % message}, host)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import os.path
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
|
|
||||||
class Plugin():
|
class Plugin(object):
|
||||||
|
|
||||||
def conf(self, attr, default = None):
|
def conf(self, attr, default = None):
|
||||||
return Env.setting(attr, self.getName().lower(), default = default)
|
return Env.setting(attr, self.getName().lower(), default = default)
|
||||||
@@ -39,4 +39,4 @@ class Plugin():
|
|||||||
return not self.isEnabled()
|
return not self.isEnabled()
|
||||||
|
|
||||||
def isEnabled(self):
|
def isEnabled(self):
|
||||||
return self.conf('enabled')
|
return self.conf('enabled') or self.conf('enabled') == None
|
||||||
|
|||||||
@@ -149,6 +149,10 @@ class Renamer(Plugin):
|
|||||||
if multiple:
|
if multiple:
|
||||||
cd += 1
|
cd += 1
|
||||||
|
|
||||||
|
# Notify on download
|
||||||
|
download_message = 'Download of %s (%s) successful.' % (group['library']['titles'][0]['title'], replacements['quality'])
|
||||||
|
fireEvent('notify', type = 'movie.downloaded', message = download_message, data = replacements)
|
||||||
|
|
||||||
# Before renaming, remove the lower quality files
|
# Before renaming, remove the lower quality files
|
||||||
db = get_session()
|
db = get_session()
|
||||||
library = db.query(Library).filter_by(identifier = group['library']['identifier']).first()
|
library = db.query(Library).filter_by(identifier = group['library']['identifier']).first()
|
||||||
@@ -172,6 +176,10 @@ class Renamer(Plugin):
|
|||||||
for rename_me in rename_files:
|
for rename_me in rename_files:
|
||||||
filename = os.path.basename(rename_me)
|
filename = os.path.basename(rename_me)
|
||||||
rename_files[rename_me] = rename_me.replace(filename, '_EXISTS_%s' % filename)
|
rename_files[rename_me] = rename_me.replace(filename, '_EXISTS_%s' % filename)
|
||||||
|
|
||||||
|
# Notify on rename fail
|
||||||
|
download_message = 'Renaming of %s (%s) canceled, exists in %s already.' % (movie.library.titles[0].title, group['meta_data']['quality']['label'], release.quality.label)
|
||||||
|
fireEvent('notify', type = 'movie.renaming.canceled', message = download_message, data = group)
|
||||||
|
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -200,7 +208,7 @@ class Renamer(Plugin):
|
|||||||
|
|
||||||
#print rename_me, rename_files[rename_me]
|
#print rename_me, rename_files[rename_me]
|
||||||
|
|
||||||
# Search for trailers
|
# Search for trailers etc
|
||||||
fireEvent('renamer.after', group)
|
fireEvent('renamer.after', group)
|
||||||
|
|
||||||
def moveFile(self, old, dest, suppress = True):
|
def moveFile(self, old, dest, suppress = True):
|
||||||
|
|||||||
@@ -91,7 +91,6 @@ class Searcher(Plugin):
|
|||||||
print successful
|
print successful
|
||||||
|
|
||||||
if successful:
|
if successful:
|
||||||
log.info('Downloading of %s successful.' % nzb.get('name'))
|
|
||||||
|
|
||||||
# Mark release as snatched
|
# Mark release as snatched
|
||||||
db = get_session()
|
db = get_session()
|
||||||
@@ -105,6 +104,9 @@ class Searcher(Plugin):
|
|||||||
mvie.status_id = snatched_status.get('id')
|
mvie.status_id = snatched_status.get('id')
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
log.info('Downloading of %s successful.' % nzb.get('name'))
|
||||||
|
fireEvent('notify', type = 'movie.snatched', message = 'Downloading of %s successful.' % nzb.get('name'), data = rls.to_dict())
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
from .main import Updater
|
||||||
|
|
||||||
|
def start():
|
||||||
|
return Updater()
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'updater',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'general',
|
||||||
|
'name': 'updater',
|
||||||
|
'label': 'Updater',
|
||||||
|
'git_only': True,
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'default': True,
|
||||||
|
'type': 'enabler',
|
||||||
|
'description': 'Enable periodic update checking',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'automatic',
|
||||||
|
'default': False,
|
||||||
|
'type': 'enabler',
|
||||||
|
'description': 'Automaticly update when update is available',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}]
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
from couchpotato.core.event import addEvent, fireEvent
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.plugins.base import Plugin
|
||||||
|
from couchpotato.environment import Env
|
||||||
|
from git.repository import LocalRepository
|
||||||
|
import time
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Updater(Plugin):
|
||||||
|
|
||||||
|
git = 'git://github.com/CouchPotato/CouchPotato.git'
|
||||||
|
|
||||||
|
running = False
|
||||||
|
version = None
|
||||||
|
updateFailed = False
|
||||||
|
updateAvailable = False
|
||||||
|
updateVersion = None
|
||||||
|
lastCheck = 0
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
|
||||||
|
self.repo = LocalRepository(Env.get('app_dir'))
|
||||||
|
|
||||||
|
fireEvent('schedule.interval', 'updater.check', self.check, hours = 6)
|
||||||
|
|
||||||
|
addEvent('app.load', self.check)
|
||||||
|
|
||||||
|
def getVersion(self):
|
||||||
|
|
||||||
|
if not self.version:
|
||||||
|
try:
|
||||||
|
output = self.repo.getHead() # Yes, please
|
||||||
|
log.debug('Git version output: %s' % output.hash)
|
||||||
|
self.version = output.hash
|
||||||
|
except Exception, e:
|
||||||
|
log.error('Failed using GIT updater, running from source, you need to have GIT installed. %s' % e)
|
||||||
|
return 'No GIT'
|
||||||
|
|
||||||
|
return self.version
|
||||||
|
|
||||||
|
def check(self):
|
||||||
|
|
||||||
|
if self.updateAvailable or self.isDisabled():
|
||||||
|
return
|
||||||
|
|
||||||
|
current_branch = self.repo.getCurrentBranch().name
|
||||||
|
|
||||||
|
for branch in self.repo.getRemoteByName('origin').getBranches():
|
||||||
|
if current_branch == branch.name:
|
||||||
|
|
||||||
|
local = self.repo.getHead()
|
||||||
|
remote = branch.getHead()
|
||||||
|
|
||||||
|
if local.getDate() < remote.getDate():
|
||||||
|
if self.conf('automatic') and not self.updateFailed:
|
||||||
|
self.doUpdate()
|
||||||
|
else:
|
||||||
|
self.updateAvailable = True
|
||||||
|
self.updateVersion = remote.hash
|
||||||
|
|
||||||
|
self.lastCheck = time.time()
|
||||||
|
|
||||||
|
def doUpdate(self):
|
||||||
|
try:
|
||||||
|
log.info('Updating to latest version');
|
||||||
|
self.repo.pull()
|
||||||
|
return True
|
||||||
|
except Exception, e:
|
||||||
|
log.error('Failed updating via GIT: %s' % e)
|
||||||
|
|
||||||
|
self.updateFailed = True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def isEnabled(self):
|
||||||
|
return Plugin.isEnabled(self) and Env.get('uses_git')
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ class Provider(Plugin):
|
|||||||
data = urllib2.urlopen(url).read()
|
data = urllib2.urlopen(url).read()
|
||||||
|
|
||||||
except IOError, e:
|
except IOError, e:
|
||||||
log.debug(e)
|
log.error('Failed opening url, %s: %s' % (url, e))
|
||||||
data = ''
|
data = ''
|
||||||
|
|
||||||
self.last_use = time.time()
|
self.last_use = time.time()
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
from .main import CouchPotatoApi
|
||||||
|
|
||||||
|
def start():
|
||||||
|
return CouchPotatoApi()
|
||||||
|
|
||||||
|
config = []
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
from couchpotato.core.event import addEvent
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.providers.base import MovieProvider
|
||||||
|
from flask.helpers import json
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class CouchPotatoApi(MovieProvider):
|
||||||
|
|
||||||
|
apiUrl = 'http://couchpotatoapp.com/api/%s/%s/'
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
|
||||||
|
addEvent('provider.movie.release_date', self.releaseDate)
|
||||||
|
|
||||||
|
def releaseDate(self, imdb_id):
|
||||||
|
|
||||||
|
data = self.urlopen(self.apiUrl % ('eta', id))
|
||||||
|
|
||||||
|
try:
|
||||||
|
dates = json.loads(data)
|
||||||
|
log.info('Found ETA for %s: %s' % (imdb_id, dates))
|
||||||
|
except Exception, e:
|
||||||
|
log.error('Error getting ETA for %s: %s' % (imdb_id, e))
|
||||||
|
|
||||||
|
return dates
|
||||||
@@ -111,7 +111,7 @@ class NZBMatrix(NZBProvider, RSS):
|
|||||||
return results
|
return results
|
||||||
|
|
||||||
def getApiExt(self):
|
def getApiExt(self):
|
||||||
return '&username=%s&apikey=%s' % (self.conf('username'), self.conf('apikey'))
|
return '&username=%s&apikey=%s' % (self.conf('username'), self.conf('api_key'))
|
||||||
|
|
||||||
def isEnabled(self):
|
def isEnabled(self):
|
||||||
return NZBProvider.isEnabled(self) and self.conf('username') and self.conf('api_key')
|
return NZBProvider.isEnabled(self) and self.conf('username') and self.conf('api_key')
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ from elixir.entity import Entity
|
|||||||
from elixir.fields import Field
|
from elixir.fields import Field
|
||||||
from elixir.options import options_defaults
|
from elixir.options import options_defaults
|
||||||
from elixir.relationships import OneToMany, ManyToOne
|
from elixir.relationships import OneToMany, ManyToOne
|
||||||
from libs.elixir.options import using_options
|
from elixir.options import using_options
|
||||||
from libs.elixir.relationships import ManyToMany
|
from elixir.relationships import ManyToMany
|
||||||
from sqlalchemy.types import Integer, Unicode, UnicodeText, Boolean, Float, \
|
from sqlalchemy.types import Integer, Unicode, UnicodeText, Boolean, Float, \
|
||||||
String
|
String
|
||||||
|
|
||||||
@@ -179,7 +179,10 @@ class History(Entity):
|
|||||||
"""History of actions that are connected to a certain release,
|
"""History of actions that are connected to a certain release,
|
||||||
such as, renamed to, downloaded, deleted, download subtitles etc"""
|
such as, renamed to, downloaded, deleted, download subtitles etc"""
|
||||||
|
|
||||||
|
added = Field(Integer)
|
||||||
message = Field(UnicodeText())
|
message = Field(UnicodeText())
|
||||||
|
type = Field(Unicode(50))
|
||||||
|
|
||||||
release = ManyToOne('Release')
|
release = ManyToOne('Release')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from couchpotato.core.settings import Settings
|
|||||||
class Env:
|
class Env:
|
||||||
|
|
||||||
''' Environment variables '''
|
''' Environment variables '''
|
||||||
|
_uses_git = False
|
||||||
_debug = False
|
_debug = False
|
||||||
_settings = Settings()
|
_settings = Settings()
|
||||||
_loader = Loader()
|
_loader = Loader()
|
||||||
|
|||||||
@@ -17,14 +17,6 @@ Page.Wanted = new Class({
|
|||||||
});
|
});
|
||||||
$(self.wanted).inject(self.el);
|
$(self.wanted).inject(self.el);
|
||||||
App.addEvent('library.update', self.wanted.update.bind(self.wanted));
|
App.addEvent('library.update', self.wanted.update.bind(self.wanted));
|
||||||
|
|
||||||
// Snatched movies
|
|
||||||
self.snatched = new MovieList({
|
|
||||||
'status': 'snatched',
|
|
||||||
'actions': SnatchedActions
|
|
||||||
});
|
|
||||||
$(self.snatched).inject(self.el);
|
|
||||||
App.addEvent('library.update', self.snatched.update.bind(self.snatched));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Copyright (c) 2009, Rotem Yaari <vmalloc@gmail.com>
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
# * Redistributions of source code must retain the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer.
|
||||||
|
# * Redistributions in binary form must reproduce the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer in the
|
||||||
|
# documentation and/or other materials provided with the distribution.
|
||||||
|
# * Neither the name of organization nor the
|
||||||
|
# names of its contributors may be used to endorse or promote products
|
||||||
|
# derived from this software without specific prior written permission.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY Rotem Yaari ''AS IS'' AND ANY
|
||||||
|
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
# DISCLAIMED. IN NO EVENT SHALL Rotem Yaari BE LIABLE FOR ANY
|
||||||
|
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||||
|
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
from repository import RemoteRepository
|
||||||
|
from repository import LocalRepository
|
||||||
|
from repository import clone
|
||||||
|
from repository import find_repository
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# Copyright (c) 2009, Rotem Yaari <vmalloc@gmail.com>
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
# * Redistributions of source code must retain the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer.
|
||||||
|
# * Redistributions in binary form must reproduce the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer in the
|
||||||
|
# documentation and/or other materials provided with the distribution.
|
||||||
|
# * Neither the name of organization nor the
|
||||||
|
# names of its contributors may be used to endorse or promote products
|
||||||
|
# derived from this software without specific prior written permission.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY Rotem Yaari ''AS IS'' AND ANY
|
||||||
|
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
# DISCLAIMED. IN NO EVENT SHALL Rotem Yaari BE LIABLE FOR ANY
|
||||||
|
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||||
|
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
import re
|
||||||
|
from ref import Ref
|
||||||
|
|
||||||
|
class Branch(Ref):
|
||||||
|
def delete(self):
|
||||||
|
raise NotImplementedError()
|
||||||
|
def __repr__(self):
|
||||||
|
return "<branch %s>" % (self.name,)
|
||||||
|
class LocalBranch(Branch):
|
||||||
|
def delete(self, force=True):
|
||||||
|
self.repo._executeGitCommandAssertSuccess("git branch -%s %s" % ("D" if force else "d", self.name,))
|
||||||
|
def setRemoteBranch(self, branch):
|
||||||
|
if branch is None:
|
||||||
|
self.repo.config.unsetParameter('branch.%s.remote' % self.name)
|
||||||
|
self.repo.config.unsetParameter('branch.%s.merge' % self.name)
|
||||||
|
return
|
||||||
|
elif not isinstance(branch, RegisteredRemoteBranch):
|
||||||
|
raise ValueError("Remote branch must be a remote branch object (got %r)" % (branch,))
|
||||||
|
self.repo.config.setParameter('branch.%s.remote' % self.name, branch.remote.name)
|
||||||
|
self.repo.config.setParameter('branch.%s.merge' % self.name, 'refs/heads/%s' % branch.name)
|
||||||
|
def getRemoteBranch(self):
|
||||||
|
remote = self.repo.config.getParameter('branch.%s.remote' % self.name)
|
||||||
|
if remote is None:
|
||||||
|
return None
|
||||||
|
remote = self.repo.getRemoteByName(remote)
|
||||||
|
merge = self.repo.config.getParameter('branch.%s.merge' % self.name)
|
||||||
|
merge = re.sub('^refs/heads/', '', merge)
|
||||||
|
remote_branch = remote.getBranchByName(merge)
|
||||||
|
return remote_branch
|
||||||
|
|
||||||
|
class LocalBranchAlias(LocalBranch):
|
||||||
|
def __init__(self, repository, name, dest):
|
||||||
|
super(LocalBranchAlias, self).__init__(repository, name)
|
||||||
|
self.dest = dest
|
||||||
|
|
||||||
|
class RemoteBranch(Branch):
|
||||||
|
pass
|
||||||
|
class RegisteredRemoteBranch(RemoteBranch):
|
||||||
|
def __init__(self, repo, remote, name):
|
||||||
|
super(RegisteredRemoteBranch, self).__init__(repo, name)
|
||||||
|
self.remote = remote
|
||||||
|
def getHead(self):
|
||||||
|
return self.repo._getCommitByRefName("%s/%s" % (self.remote.name, self.name))
|
||||||
|
def delete(self):
|
||||||
|
"""
|
||||||
|
Deletes the actual branch on the remote repository!
|
||||||
|
"""
|
||||||
|
self.repo.push(self.remote, fromBranch="", toBranch=self, force=True)
|
||||||
|
def getNormalizedName(self):
|
||||||
|
return "%s/%s" % (self.remote.name, self.name)
|
||||||
|
def __repr__(self):
|
||||||
|
return "<branch %s on remote %r>" % (self.name, self.remote.name)
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
# Copyright (c) 2009, Rotem Yaari <vmalloc@gmail.com>
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
# * Redistributions of source code must retain the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer.
|
||||||
|
# * Redistributions in binary form must reproduce the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer in the
|
||||||
|
# documentation and/or other materials provided with the distribution.
|
||||||
|
# * Neither the name of organization nor the
|
||||||
|
# names of its contributors may be used to endorse or promote products
|
||||||
|
# derived from this software without specific prior written permission.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY Rotem Yaari ''AS IS'' AND ANY
|
||||||
|
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
# DISCLAIMED. IN NO EVENT SHALL Rotem Yaari BE LIABLE FOR ANY
|
||||||
|
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||||
|
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
from .ref import Ref
|
||||||
|
from .files import ModifiedFile
|
||||||
|
|
||||||
|
SHA1_LENGTH = 40
|
||||||
|
|
||||||
|
class Commit(Ref):
|
||||||
|
def __init__(self, repo, sha):
|
||||||
|
sha = str(sha).lower()
|
||||||
|
if len(sha) < SHA1_LENGTH:
|
||||||
|
sha = repo._getCommitByPartialHash(sha).hash
|
||||||
|
super(Commit, self).__init__(repo, sha)
|
||||||
|
self.hash = sha
|
||||||
|
def __repr__(self):
|
||||||
|
return self.hash
|
||||||
|
def __eq__(self, other):
|
||||||
|
if not isinstance(other, Commit):
|
||||||
|
if isinstance(other, Ref):
|
||||||
|
other = other.getHead().hash
|
||||||
|
else:
|
||||||
|
other = other.hash
|
||||||
|
if other is None:
|
||||||
|
return False
|
||||||
|
if not isinstance(other, basestring):
|
||||||
|
raise TypeError("Comparing %s and %s" % (type(self), type(other)))
|
||||||
|
return (self.hash == other.lower())
|
||||||
|
def getParents(self):
|
||||||
|
output = self.repo._getOutputAssertSuccess("git rev-list %s --parents -1" % self)
|
||||||
|
return [Commit(self.repo, sha.strip()) for sha in output.split()[1:]]
|
||||||
|
def getChange(self):
|
||||||
|
returned = []
|
||||||
|
for line in self.repo._getOutputAssertSuccess("git show --pretty=format: --raw %s" % self).splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
filename = line.split()[-1]
|
||||||
|
returned.append(ModifiedFile(filename))
|
||||||
|
return returned
|
||||||
|
getChangedFiles = getChange
|
||||||
|
############################ Misc. Commit attributes ###########################
|
||||||
|
def _getCommitField(self, field):
|
||||||
|
return self.repo._executeGitCommandAssertSuccess("git log -1 --pretty=format:%s %s" % (field, self)).stdout.read().strip()
|
||||||
|
def getAuthorName(self):
|
||||||
|
return self._getCommitField("%an")
|
||||||
|
def getAuthorEmail(self):
|
||||||
|
return self._getCommitField("%ae")
|
||||||
|
def getDate(self):
|
||||||
|
return int(self._getCommitField("%at"))
|
||||||
|
def getSubject(self):
|
||||||
|
return self._getCommitField("%s")
|
||||||
|
def getMessageBody(self):
|
||||||
|
return self._getCommitField("%b")
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# Copyright (c) 2009, Rotem Yaari <vmalloc@gmail.com>
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
# * Redistributions of source code must retain the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer.
|
||||||
|
# * Redistributions in binary form must reproduce the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer in the
|
||||||
|
# documentation and/or other materials provided with the distribution.
|
||||||
|
# * Neither the name of organization nor the
|
||||||
|
# names of its contributors may be used to endorse or promote products
|
||||||
|
# derived from this software without specific prior written permission.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY Rotem Yaari ''AS IS'' AND ANY
|
||||||
|
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
# DISCLAIMED. IN NO EVENT SHALL Rotem Yaari BE LIABLE FOR ANY
|
||||||
|
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||||
|
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
from .exceptions import GitCommandFailedException
|
||||||
|
|
||||||
|
class GitConfiguration(object):
|
||||||
|
def __init__(self, repo):
|
||||||
|
super(GitConfiguration, self).__init__()
|
||||||
|
self.repo = repo
|
||||||
|
def setParameter(self, path, value, local=True):
|
||||||
|
self.repo._executeGitCommandAssertSuccess("git config %s \"%s\" \"%s\"" % ("" if local else "--global", path, value))
|
||||||
|
def unsetParameter(self, path, local=True):
|
||||||
|
try:
|
||||||
|
self.repo._executeGitCommandAssertSuccess("git config --unset %s \"%s\"" % ("" if local else "--global", path))
|
||||||
|
except GitCommandFailedException:
|
||||||
|
if self.getParameter(path) is not None:
|
||||||
|
raise
|
||||||
|
def getParameter(self, path):
|
||||||
|
return self.getDict().get(path, None)
|
||||||
|
def getDict(self):
|
||||||
|
return dict(line.strip().split("=",1)
|
||||||
|
for line in self.repo._getOutputAssertSuccess("git config -l").splitlines())
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# Copyright (c) 2009, Rotem Yaari <vmalloc@gmail.com>
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
# * Redistributions of source code must retain the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer.
|
||||||
|
# * Redistributions in binary form must reproduce the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer in the
|
||||||
|
# documentation and/or other materials provided with the distribution.
|
||||||
|
# * Neither the name of organization nor the
|
||||||
|
# names of its contributors may be used to endorse or promote products
|
||||||
|
# derived from this software without specific prior written permission.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY Rotem Yaari ''AS IS'' AND ANY
|
||||||
|
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
# DISCLAIMED. IN NO EVENT SHALL Rotem Yaari BE LIABLE FOR ANY
|
||||||
|
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||||
|
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
import os
|
||||||
|
|
||||||
|
class GitException(Exception):
|
||||||
|
def __init__(self, msg):
|
||||||
|
super(GitException, self).__init__()
|
||||||
|
self.msg = msg
|
||||||
|
def __repr__(self):
|
||||||
|
return "%s: %s" % (type(self).__name__, self.msg)
|
||||||
|
__str__ = __repr__
|
||||||
|
|
||||||
|
class CannotFindRepository(GitException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class MergeConflict(GitException):
|
||||||
|
def __init__(self, msg='Merge Conflict'):
|
||||||
|
super(MergeConflict, self).__init__(msg=msg)
|
||||||
|
|
||||||
|
class GitCommandFailedException(GitException):
|
||||||
|
def __init__(self, directory, command, popen):
|
||||||
|
super(GitCommandFailedException, self).__init__(None)
|
||||||
|
self.command = command
|
||||||
|
self.directory = os.path.abspath(directory)
|
||||||
|
self.stderr = popen.stderr.read()
|
||||||
|
self.stdout = popen.stdout.read()
|
||||||
|
self.popen = popen
|
||||||
|
self.msg = "Command %r failed in %s (%s):\n%s\n%s" % (command, self.directory, popen.returncode,
|
||||||
|
self.stderr, self.stdout)
|
||||||
|
class NonexistentRefException(GitException):
|
||||||
|
pass
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# Copyright (c) 2009, Rotem Yaari <vmalloc@gmail.com>
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
# * Redistributions of source code must retain the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer.
|
||||||
|
# * Redistributions in binary form must reproduce the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer in the
|
||||||
|
# documentation and/or other materials provided with the distribution.
|
||||||
|
# * Neither the name of organization nor the
|
||||||
|
# names of its contributors may be used to endorse or promote products
|
||||||
|
# derived from this software without specific prior written permission.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY Rotem Yaari ''AS IS'' AND ANY
|
||||||
|
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
# DISCLAIMED. IN NO EVENT SHALL Rotem Yaari BE LIABLE FOR ANY
|
||||||
|
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||||
|
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
class ModifiedFile(object):
|
||||||
|
def __init__(self, filename):
|
||||||
|
super(ModifiedFile, self).__init__()
|
||||||
|
self.filename = filename
|
||||||
|
def __repr__(self):
|
||||||
|
return self.filename
|
||||||
|
def __eq__(self, other):
|
||||||
|
return isinstance(other, ModifiedFile) and other.filename == self.filename
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# Copyright (c) 2009, Rotem Yaari <vmalloc@gmail.com>
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
# * Redistributions of source code must retain the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer.
|
||||||
|
# * Redistributions in binary form must reproduce the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer in the
|
||||||
|
# documentation and/or other materials provided with the distribution.
|
||||||
|
# * Neither the name of organization nor the
|
||||||
|
# names of its contributors may be used to endorse or promote products
|
||||||
|
# derived from this software without specific prior written permission.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY Rotem Yaari ''AS IS'' AND ANY
|
||||||
|
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
# DISCLAIMED. IN NO EVENT SHALL Rotem Yaari BE LIABLE FOR ANY
|
||||||
|
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||||
|
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
class Ref(object):
|
||||||
|
def __init__(self, repo, name):
|
||||||
|
super(Ref, self).__init__()
|
||||||
|
self.repo = repo
|
||||||
|
self.name = name
|
||||||
|
def getHead(self):
|
||||||
|
return self.repo._getCommitByRefName(self.name)
|
||||||
|
def getNormalizedName(self):
|
||||||
|
return self.name
|
||||||
|
def getNewCommits(self, comparedTo, limit=""):
|
||||||
|
returned = []
|
||||||
|
command = "git cherry %s %s %s" % (self.repo._normalizeRefName(comparedTo),
|
||||||
|
self.getNormalizedName(),
|
||||||
|
self.repo._normalizeRefName(limit))
|
||||||
|
for line in self.repo._getOutputAssertSuccess(command).splitlines():
|
||||||
|
symbol, sha = line.split()
|
||||||
|
if symbol == '-':
|
||||||
|
#already has an equivalent commit
|
||||||
|
continue
|
||||||
|
returned.append(self.repo._getCommitByHash(sha.strip()))
|
||||||
|
return returned
|
||||||
|
def __eq__(self, ref):
|
||||||
|
return (type(ref) is type(self) and ref.name == self.name)
|
||||||
|
def __ne__(self, ref):
|
||||||
|
return not (self == ref)
|
||||||
|
def __repr__(self):
|
||||||
|
return "<%s %s>" % (type(self).__name__, self.getNormalizedName())
|
||||||
|
################################## Containment #################################
|
||||||
|
def getMergeBase(self, other):
|
||||||
|
return self.repo.getMergeBase(self, other)
|
||||||
|
__and__ = getMergeBase
|
||||||
|
def contains(self, other):
|
||||||
|
return self.getMergeBase(other) == other
|
||||||
|
__contains__ = contains
|
||||||
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Copyright (c) 2009, Rotem Yaari <vmalloc@gmail.com>
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
# * Redistributions of source code must retain the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer.
|
||||||
|
# * Redistributions in binary form must reproduce the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer in the
|
||||||
|
# documentation and/or other materials provided with the distribution.
|
||||||
|
# * Neither the name of organization nor the
|
||||||
|
# names of its contributors may be used to endorse or promote products
|
||||||
|
# derived from this software without specific prior written permission.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY Rotem Yaari ''AS IS'' AND ANY
|
||||||
|
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
# DISCLAIMED. IN NO EVENT SHALL Rotem Yaari BE LIABLE FOR ANY
|
||||||
|
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||||
|
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
from . import exceptions
|
||||||
|
|
||||||
|
class RefContainer(object):
|
||||||
|
def getBranches(self):
|
||||||
|
raise NotImplementedError()
|
||||||
|
def getTags(self):
|
||||||
|
raise NotImplementedError()
|
||||||
|
########################### Looking for specific refs ##########################
|
||||||
|
def _getByName(self, func, name):
|
||||||
|
for ref in func():
|
||||||
|
if ref.name == name:
|
||||||
|
return ref
|
||||||
|
raise exceptions.NonexistentRefException(name)
|
||||||
|
def getBranchByName(self, name):
|
||||||
|
return self._getByName(self.getBranches, name)
|
||||||
|
def hasBranch(self, name):
|
||||||
|
try:
|
||||||
|
self.getBranchByName(name)
|
||||||
|
return True
|
||||||
|
except exceptions.NonexistentRefException:
|
||||||
|
return False
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# Copyright (c) 2009, Rotem Yaari <vmalloc@gmail.com>
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
# * Redistributions of source code must retain the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer.
|
||||||
|
# * Redistributions in binary form must reproduce the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer in the
|
||||||
|
# documentation and/or other materials provided with the distribution.
|
||||||
|
# * Neither the name of organization nor the
|
||||||
|
# names of its contributors may be used to endorse or promote products
|
||||||
|
# derived from this software without specific prior written permission.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY Rotem Yaari ''AS IS'' AND ANY
|
||||||
|
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
# DISCLAIMED. IN NO EVENT SHALL Rotem Yaari BE LIABLE FOR ANY
|
||||||
|
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||||
|
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
from . import branch
|
||||||
|
from . import ref_container
|
||||||
|
|
||||||
|
class Remote(ref_container.RefContainer):
|
||||||
|
def __init__(self, repo, name, url):
|
||||||
|
super(Remote, self).__init__()
|
||||||
|
self.repo = repo
|
||||||
|
self.name = name
|
||||||
|
self.url = url
|
||||||
|
def fetch(self):
|
||||||
|
self.repo._executeGitCommandAssertSuccess("git fetch %s" % self.name)
|
||||||
|
def prune(self):
|
||||||
|
self.repo._executeGitCommandAssertSuccess("git remote prune %s" % self.name)
|
||||||
|
def __eq__(self, other):
|
||||||
|
return (type(self) is type(other)) and (self.name == other.name)
|
||||||
|
###################### For compatibility with RefContainer #####################
|
||||||
|
def getBranches(self):
|
||||||
|
prefix = "%s/" % self.name
|
||||||
|
returned = []
|
||||||
|
for line in self.repo._getOutputAssertSuccess("git branch -r").splitlines():
|
||||||
|
if self.repo.getGitVersion() >= '1.6.3' and ' -> ' in line:
|
||||||
|
continue
|
||||||
|
line = line.strip()
|
||||||
|
if line.startswith(prefix):
|
||||||
|
returned.append(branch.RegisteredRemoteBranch(self.repo, self, line[len(prefix):]))
|
||||||
|
return returned
|
||||||
|
|
||||||
@@ -0,0 +1,399 @@
|
|||||||
|
# Copyright (c) 2009, Rotem Yaari <vmalloc@gmail.com>
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
# * Redistributions of source code must retain the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer.
|
||||||
|
# * Redistributions in binary form must reproduce the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer in the
|
||||||
|
# documentation and/or other materials provided with the distribution.
|
||||||
|
# * Neither the name of organization nor the
|
||||||
|
# names of its contributors may be used to endorse or promote products
|
||||||
|
# derived from this software without specific prior written permission.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY Rotem Yaari ''AS IS'' AND ANY
|
||||||
|
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
# DISCLAIMED. IN NO EVENT SHALL Rotem Yaari BE LIABLE FOR ANY
|
||||||
|
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||||
|
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from . import branch
|
||||||
|
from . import commit
|
||||||
|
from . import config
|
||||||
|
from .files import ModifiedFile
|
||||||
|
from . import ref
|
||||||
|
from . import ref_container
|
||||||
|
from . import remotes
|
||||||
|
from .utils import quote_for_shell
|
||||||
|
from .utils import CommandString as CMD
|
||||||
|
|
||||||
|
#exceptions
|
||||||
|
from .exceptions import CannotFindRepository
|
||||||
|
from .exceptions import GitException
|
||||||
|
from .exceptions import GitCommandFailedException
|
||||||
|
from .exceptions import MergeConflict
|
||||||
|
|
||||||
|
BRANCH_ALIAS_MARKER = ' -> '
|
||||||
|
|
||||||
|
class Repository(ref_container.RefContainer):
|
||||||
|
############################# internal methods #############################
|
||||||
|
_loggingEnabled = False
|
||||||
|
def _getWorkingDirectory(self):
|
||||||
|
return '.'
|
||||||
|
def _logGitCommand(self, command, cwd):
|
||||||
|
if self._loggingEnabled:
|
||||||
|
print >> sys.stderr, ">>", command
|
||||||
|
def enableLogging(self):
|
||||||
|
self._loggingEnabled = True
|
||||||
|
def disableLogging(self):
|
||||||
|
self._loggingEnabled = False
|
||||||
|
def _executeGitCommand(self, command, cwd=None):
|
||||||
|
if cwd is None:
|
||||||
|
cwd = self._getWorkingDirectory()
|
||||||
|
command = str(command)
|
||||||
|
self._logGitCommand(command, cwd)
|
||||||
|
returned = subprocess.Popen(command,
|
||||||
|
shell=True,
|
||||||
|
cwd=cwd,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE)
|
||||||
|
returned.wait()
|
||||||
|
return returned
|
||||||
|
def _executeGitCommandAssertSuccess(self, command, **kwargs):
|
||||||
|
returned = self._executeGitCommand(command, **kwargs)
|
||||||
|
assert returned.returncode is not None
|
||||||
|
if returned.returncode != 0:
|
||||||
|
raise GitCommandFailedException(kwargs.get('cwd', self._getWorkingDirectory()), command, returned)
|
||||||
|
return returned
|
||||||
|
def _getOutputAssertSuccess(self, command, **kwargs):
|
||||||
|
return self._executeGitCommandAssertSuccess(command, **kwargs).stdout.read()
|
||||||
|
def _getMergeBase(self, a, b):
|
||||||
|
raise NotImplementedError()
|
||||||
|
def getMergeBase(self, a, b):
|
||||||
|
repo = self
|
||||||
|
if isinstance(b, commit.Commit) and isinstance(b.repo, LocalRepository):
|
||||||
|
repo = b.repo
|
||||||
|
elif isinstance(a, commit.Commit) and isinstance(a.repo, LocalRepository):
|
||||||
|
repo = a.repo
|
||||||
|
return repo._getMergeBase(a, b)
|
||||||
|
|
||||||
|
|
||||||
|
############################## remote repositories #############################
|
||||||
|
class RemoteRepository(Repository):
|
||||||
|
def __init__(self, url):
|
||||||
|
super(RemoteRepository, self).__init__()
|
||||||
|
self.url = url
|
||||||
|
def _getRefs(self, prefix):
|
||||||
|
output = self._executeGitCommandAssertSuccess("git ls-remote %s" % (self.url,))
|
||||||
|
for output_line in output.stdout:
|
||||||
|
commit, refname = output_line.split()
|
||||||
|
if refname.startswith(prefix):
|
||||||
|
yield refname[len(prefix):]
|
||||||
|
def _getRefsAsClass(self, prefix, cls):
|
||||||
|
return [cls(self, ref) for ref in self._getRefs(prefix)]
|
||||||
|
def getBranches(self):
|
||||||
|
return self._getRefsAsClass('refs/heads/', branch.RemoteBranch)
|
||||||
|
############################## local repositories ##############################
|
||||||
|
class LocalRepository(Repository):
|
||||||
|
def __init__(self, path):
|
||||||
|
super(LocalRepository, self).__init__()
|
||||||
|
self.path = path
|
||||||
|
self.config = config.GitConfiguration(self)
|
||||||
|
self._version = None
|
||||||
|
def __repr__(self):
|
||||||
|
return "<Git Repository at %s>" % (self.path,)
|
||||||
|
def _getWorkingDirectory(self):
|
||||||
|
return self.path
|
||||||
|
def _getCommitByHash(self, sha):
|
||||||
|
return commit.Commit(self, sha)
|
||||||
|
def _getCommitByRefName(self, name):
|
||||||
|
return commit.Commit(self, self._getOutputAssertSuccess("git rev-parse %s" % name).strip())
|
||||||
|
def _getCommitByPartialHash(self, sha):
|
||||||
|
return self._getCommitByRefName(sha)
|
||||||
|
def getGitVersion(self):
|
||||||
|
if self._version is None:
|
||||||
|
version_output = self._getOutputAssertSuccess("git version")
|
||||||
|
version_match = re.match(r"git\s+version\s+(\S+)$", version_output, re.I)
|
||||||
|
if version_match is None:
|
||||||
|
raise GitException("Cannot extract git version (unfamiliar output format %r?)" % version_output)
|
||||||
|
self._version = version_match.group(1)
|
||||||
|
return self._version
|
||||||
|
########################### Initializing a repository ##########################
|
||||||
|
def init(self, bare=False):
|
||||||
|
if not os.path.exists(self.path):
|
||||||
|
os.mkdir(self.path)
|
||||||
|
if not os.path.isdir(self.path):
|
||||||
|
raise GitException("Cannot create repository in %s - "
|
||||||
|
"not a directory" % self.path)
|
||||||
|
self._executeGitCommandAssertSuccess("git init %s" % ("--bare" if bare else ""))
|
||||||
|
def _asURL(self, repo):
|
||||||
|
if isinstance(repo, LocalRepository):
|
||||||
|
repo = repo.path
|
||||||
|
elif isinstance(repo, RemoteRepository):
|
||||||
|
repo = repo.url
|
||||||
|
elif not isinstance(repo, basestring):
|
||||||
|
raise TypeError("Cannot clone from %r" % (repo,))
|
||||||
|
return repo
|
||||||
|
def clone(self, repo):
|
||||||
|
self._executeGitCommandAssertSuccess("git clone %s %s" % (self._asURL(repo), self.path), cwd=".")
|
||||||
|
########################### Querying repository refs ###########################
|
||||||
|
def getBranches(self):
|
||||||
|
returned = []
|
||||||
|
for git_branch_line in self._executeGitCommandAssertSuccess("git branch").stdout:
|
||||||
|
if git_branch_line.startswith("*"):
|
||||||
|
git_branch_line = git_branch_line[1:]
|
||||||
|
git_branch_line = git_branch_line.strip()
|
||||||
|
if BRANCH_ALIAS_MARKER in git_branch_line:
|
||||||
|
alias_name, aliased = git_branch_line.split(BRANCH_ALIAS_MARKER)
|
||||||
|
returned.append(branch.LocalBranchAlias(self, alias_name, aliased))
|
||||||
|
else:
|
||||||
|
returned.append(branch.LocalBranch(self, git_branch_line))
|
||||||
|
return returned
|
||||||
|
def _getCommits(self, specs, includeMerges):
|
||||||
|
command = "git log --pretty=format:%%H %s" % specs
|
||||||
|
if not includeMerges:
|
||||||
|
command += " --no-merges"
|
||||||
|
for c in self._executeGitCommandAssertSuccess(command).stdout:
|
||||||
|
yield commit.Commit(self, c.strip())
|
||||||
|
def getCommits(self, start=None, end="HEAD", includeMerges=True):
|
||||||
|
spec = self._normalizeRefName(start or "")
|
||||||
|
spec += ".."
|
||||||
|
spec += self._normalizeRefName(end)
|
||||||
|
return list(self._getCommits(spec, includeMerges=includeMerges))
|
||||||
|
def getCurrentBranch(self):
|
||||||
|
#todo: improve this method of obtaining current branch
|
||||||
|
for branch_name in self._executeGitCommandAssertSuccess("git branch").stdout:
|
||||||
|
branch_name = branch_name.strip()
|
||||||
|
if not branch_name.startswith("*"):
|
||||||
|
continue
|
||||||
|
branch_name = branch_name[1:].strip()
|
||||||
|
if branch_name == '(no branch)':
|
||||||
|
return None
|
||||||
|
return self.getBranchByName(branch_name)
|
||||||
|
def getRemotes(self):
|
||||||
|
config_dict = self.config.getDict()
|
||||||
|
returned = []
|
||||||
|
for line in self._getOutputAssertSuccess("git remote show -n").splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
returned.append(remotes.Remote(self, line, config_dict.get('remote.%s.url' % line.strip())))
|
||||||
|
return returned
|
||||||
|
def getRemoteByName(self, name):
|
||||||
|
return self._getByName(self.getRemotes, name)
|
||||||
|
def _getMergeBase(self, a, b):
|
||||||
|
if isinstance(a, ref.Ref):
|
||||||
|
a = a.getHead()
|
||||||
|
if isinstance(b, ref.Ref):
|
||||||
|
b = b.getHead()
|
||||||
|
returned = self._executeGitCommand("git merge-base %s %s" % (a, b))
|
||||||
|
if returned.returncode == 0:
|
||||||
|
return commit.Commit(self, returned.stdout.read().strip())
|
||||||
|
# make sure this is not a misc. error with git
|
||||||
|
unused = self.getHead()
|
||||||
|
return None
|
||||||
|
################################ Querying Status ###############################
|
||||||
|
def containsCommit(self, commit):
|
||||||
|
try:
|
||||||
|
self._executeGitCommandAssertSuccess("git log -1 %s" % (commit,))
|
||||||
|
except GitException:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
def getHead(self):
|
||||||
|
return self._getCommitByRefName("HEAD")
|
||||||
|
def _getFiles(self, *flags):
|
||||||
|
flags = ["--exclude-standard"] + list(flags)
|
||||||
|
return [f.strip()
|
||||||
|
for f in self._getOutputAssertSuccess("git ls-files %s" % (" ".join(flags))).splitlines()]
|
||||||
|
def _getRawDiff(self, *flags):
|
||||||
|
flags = " ".join(str(f) for f in flags)
|
||||||
|
return [ModifiedFile(line.split()[-1]) for line in
|
||||||
|
self._getOutputAssertSuccess("git diff --raw %s" % flags).splitlines()]
|
||||||
|
def getStagedFiles(self):
|
||||||
|
if self.isInitialized():
|
||||||
|
return self._getRawDiff('--cached')
|
||||||
|
return self._getFiles()
|
||||||
|
def getUnchangedFiles(self):
|
||||||
|
return self._getFiles()
|
||||||
|
def getChangedFiles(self):
|
||||||
|
return self._getRawDiff()
|
||||||
|
def getUntrackedFiles(self):
|
||||||
|
return self._getFiles("--others")
|
||||||
|
def isInitialized(self):
|
||||||
|
try:
|
||||||
|
self.getHead()
|
||||||
|
return True
|
||||||
|
except GitException:
|
||||||
|
return False
|
||||||
|
def isValid(self):
|
||||||
|
return os.path.isdir(os.path.join(self.path, ".git")) or \
|
||||||
|
(os.path.isfile(os.path.join(self.path, "HEAD")) and os.path.isdir(os.path.join(self.path, "objects")))
|
||||||
|
def isWorkingDirectoryClean(self):
|
||||||
|
return not (self.getUntrackedFiles() or self.getChangedFiles() or self.getStagedFiles())
|
||||||
|
def __contains__(self, thing):
|
||||||
|
if isinstance(thing, basestring) or isinstance(thing, commit.Commit):
|
||||||
|
return self.containsCommit(thing)
|
||||||
|
raise NotImplementedError()
|
||||||
|
################################ Staging content ###############################
|
||||||
|
def add(self, path):
|
||||||
|
self._executeGitCommandAssertSuccess("git add %s" % quote_for_shell(path))
|
||||||
|
def addAll(self):
|
||||||
|
return self.add('.')
|
||||||
|
################################## Committing ##################################
|
||||||
|
def _normalizeRefName(self, thing):
|
||||||
|
if isinstance(thing, ref.Ref):
|
||||||
|
thing = thing.getNormalizedName()
|
||||||
|
return str(thing)
|
||||||
|
def _deduceNewCommitFromCommitOutput(self, output):
|
||||||
|
for pattern in [
|
||||||
|
# new-style commit pattern
|
||||||
|
r"^\[\S+\s+(?:\(root-commit\)\s+)?(\S+)\]",
|
||||||
|
]:
|
||||||
|
match = re.search(pattern, output)
|
||||||
|
if match:
|
||||||
|
return commit.Commit(self, match.group(1))
|
||||||
|
return None
|
||||||
|
def commit(self, message, allowEmpty=False):
|
||||||
|
command = "git commit -m %s" % quote_for_shell(message)
|
||||||
|
if allowEmpty:
|
||||||
|
command += " --allow-empty"
|
||||||
|
output = self._getOutputAssertSuccess(command)
|
||||||
|
return self._deduceNewCommitFromCommitOutput(output)
|
||||||
|
################################ Changing state ################################
|
||||||
|
def createBranch(self, name, startingPoint=None):
|
||||||
|
command = "git branch %s " % name
|
||||||
|
if startingPoint is not None:
|
||||||
|
command += self._normalizeRefName(startingPoint)
|
||||||
|
self._executeGitCommandAssertSuccess(command)
|
||||||
|
return branch.LocalBranch(self, name)
|
||||||
|
def checkout(self, thing=None, targetBranch=None, files=()):
|
||||||
|
if thing is None:
|
||||||
|
thing = ""
|
||||||
|
command = "git checkout %s" % (self._normalizeRefName(thing),)
|
||||||
|
if targetBranch is not None:
|
||||||
|
command += " -b %s" % (targetBranch,)
|
||||||
|
if files:
|
||||||
|
command += " -- %s" % " ".join(files)
|
||||||
|
self._executeGitCommandAssertSuccess(command)
|
||||||
|
def mergeMultiple(self, srcs, allowFastForward=True, log=False, message=None):
|
||||||
|
try:
|
||||||
|
self._executeGitCommandAssertSuccess(CMD("git merge",
|
||||||
|
" ".join(self._normalizeRefName(src) for src in srcs),
|
||||||
|
"--no-ff" if not allowFastForward else None,
|
||||||
|
"--log" if log else None,
|
||||||
|
("-m \"%s\"" % message) if message is not None else None))
|
||||||
|
except GitCommandFailedException, e:
|
||||||
|
# git-merge tends to ignore the stderr rule...
|
||||||
|
output = e.stdout + e.stderr
|
||||||
|
if 'conflict' in output.lower():
|
||||||
|
raise MergeConflict()
|
||||||
|
raise
|
||||||
|
def merge(self, src, *args, **kwargs):
|
||||||
|
return self.mergeMultiple([src], *args, **kwargs)
|
||||||
|
def _reset(self, flag, thing):
|
||||||
|
command = "git reset %s %s" % (
|
||||||
|
flag,
|
||||||
|
self._normalizeRefName(thing))
|
||||||
|
self._executeGitCommandAssertSuccess(command)
|
||||||
|
def resetSoft(self, thing="HEAD"):
|
||||||
|
return self._reset("--soft", thing)
|
||||||
|
def resetHard(self, thing="HEAD"):
|
||||||
|
return self._reset("--hard", thing)
|
||||||
|
def resetMixed(self, thing="HEAD"):
|
||||||
|
return self._reset("--mixed", thing)
|
||||||
|
def _clean(self, flags):
|
||||||
|
self._executeGitCommandAssertSuccess("git clean -q " + flags)
|
||||||
|
def cleanIgnoredFiles(self):
|
||||||
|
"""Cleans files that match the patterns in .gitignore"""
|
||||||
|
return self._clean("-f -X")
|
||||||
|
def cleanUntrackedFiles(self):
|
||||||
|
return self._clean("-f -d")
|
||||||
|
################################# collaboration ################################
|
||||||
|
def addRemote(self, name, url):
|
||||||
|
self._executeGitCommandAssertSuccess("git remote add %s %s" % (name, url))
|
||||||
|
return remotes.Remote(self, name, url)
|
||||||
|
def fetch(self, repo=None):
|
||||||
|
command = "git fetch"
|
||||||
|
if repo is not None:
|
||||||
|
command += " "
|
||||||
|
command += self._asURL(repo)
|
||||||
|
self._executeGitCommandAssertSuccess(command)
|
||||||
|
def pull(self, repo=None):
|
||||||
|
command = "git pull"
|
||||||
|
if repo is not None:
|
||||||
|
command += " "
|
||||||
|
command += self._asURL(repo)
|
||||||
|
self._executeGitCommandAssertSuccess(command)
|
||||||
|
def _getRefspec(self, fromBranch=None, toBranch=None, force=False):
|
||||||
|
returned = ""
|
||||||
|
if fromBranch is not None:
|
||||||
|
returned += self._normalizeRefName(fromBranch)
|
||||||
|
if returned or toBranch is not None:
|
||||||
|
returned += ":"
|
||||||
|
if toBranch is not None:
|
||||||
|
if isinstance(toBranch, branch.RegisteredRemoteBranch):
|
||||||
|
toBranch = toBranch.name
|
||||||
|
returned += self._normalizeRefName(toBranch)
|
||||||
|
if returned and force:
|
||||||
|
returned = "+%s" % returned
|
||||||
|
return returned
|
||||||
|
def push(self, remote=None, fromBranch=None, toBranch=None, force=False):
|
||||||
|
command = "git push"
|
||||||
|
#build push arguments
|
||||||
|
refspec = self._getRefspec(toBranch=toBranch, fromBranch=fromBranch, force=force)
|
||||||
|
|
||||||
|
if refspec and not remote:
|
||||||
|
remote = "origin"
|
||||||
|
if isinstance(remote, remotes.Remote):
|
||||||
|
remote = remote.name
|
||||||
|
elif isinstance(remote, RemoteRepository):
|
||||||
|
remote = remote.url
|
||||||
|
elif isinstance(remote, LocalRepository):
|
||||||
|
remote = remote.path
|
||||||
|
if remote is not None and not isinstance(remote, basestring):
|
||||||
|
raise TypeError("Invalid type for 'remote' parameter: %s" % (type(remote),))
|
||||||
|
command = "git push %s %s" % (remote if remote is not None else "", refspec)
|
||||||
|
self._executeGitCommandAssertSuccess(command)
|
||||||
|
def rebase(self, src):
|
||||||
|
self._executeGitCommandAssertSuccess("git rebase %s" % self._normalizeRefName(src))
|
||||||
|
#################################### Stashes ###################################
|
||||||
|
def saveStash(self, name=None):
|
||||||
|
command = "git stash save"
|
||||||
|
if name is not None:
|
||||||
|
command += " %s" % name
|
||||||
|
self._executeGitCommandAssertSuccess(command)
|
||||||
|
def popStash(self, arg=None):
|
||||||
|
command = "git stash pop"
|
||||||
|
if arg is not None:
|
||||||
|
command += " %s" % arg
|
||||||
|
self._executeGitCommandAssertSuccess(command)
|
||||||
|
################################# Configuration ################################
|
||||||
|
def getConfig(self):
|
||||||
|
return dict(s.split("=",1) for s in self._getOutputAssertSuccess("git config -l"))
|
||||||
|
|
||||||
|
################################### Shortcuts ##################################
|
||||||
|
def clone(source, location):
|
||||||
|
returned = LocalRepository(location)
|
||||||
|
returned.clone(source)
|
||||||
|
return returned
|
||||||
|
|
||||||
|
def find_repository():
|
||||||
|
orig_path = path = os.path.realpath('.')
|
||||||
|
drive, path = os.path.splitdrive(path)
|
||||||
|
while path:
|
||||||
|
current_path = os.path.join(drive, path)
|
||||||
|
current_repo = LocalRepository(current_path)
|
||||||
|
if current_repo.isValid():
|
||||||
|
return current_repo
|
||||||
|
path, path_tail = os.path.split(current_path)
|
||||||
|
if not path_tail:
|
||||||
|
raise CannotFindRepository("Cannot find repository for %s" % (orig_path,))
|
||||||
|
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# Copyright (c) 2009, Rotem Yaari <vmalloc@gmail.com>
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
# * Redistributions of source code must retain the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer.
|
||||||
|
# * Redistributions in binary form must reproduce the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer in the
|
||||||
|
# documentation and/or other materials provided with the distribution.
|
||||||
|
# * Neither the name of organization nor the
|
||||||
|
# names of its contributors may be used to endorse or promote products
|
||||||
|
# derived from this software without specific prior written permission.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY Rotem Yaari ''AS IS'' AND ANY
|
||||||
|
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
# DISCLAIMED. IN NO EVENT SHALL Rotem Yaari BE LIABLE FOR ANY
|
||||||
|
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||||
|
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
class CommandString(object):
|
||||||
|
"""
|
||||||
|
>>> CommandString('a', 'b', 'c')
|
||||||
|
a b c
|
||||||
|
>>> CommandString('a', 'b', None, 'c')
|
||||||
|
a b c
|
||||||
|
"""
|
||||||
|
def __init__(self, *args):
|
||||||
|
self.command = ""
|
||||||
|
for arg in args:
|
||||||
|
if not arg:
|
||||||
|
continue
|
||||||
|
if self.command:
|
||||||
|
self.command += " "
|
||||||
|
self.command += str(arg)
|
||||||
|
def __repr__(self):
|
||||||
|
return self.command
|
||||||
|
|
||||||
|
def quote_for_shell(s):
|
||||||
|
"""
|
||||||
|
>>> print quote_for_shell('this is a " string')
|
||||||
|
"this is a \\" string"
|
||||||
|
>>> print quote_for_shell('this is a $shell variable')
|
||||||
|
"this is a \\$shell variable"
|
||||||
|
>>> print quote_for_shell(r'an escaped \\$')
|
||||||
|
"an escaped \\\\\\$"
|
||||||
|
"""
|
||||||
|
returned = s.replace("\\", "\\\\").replace('"', '\\"').replace("$", "\\$")
|
||||||
|
if " " in returned:
|
||||||
|
returned = '"%s"' % returned
|
||||||
|
return returned
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import doctest
|
||||||
|
doctest.testmod()
|
||||||
Reference in New Issue
Block a user