Merge branch 'master' of github.com:CouchPotato/CouchPotato

Conflicts:
	couchpotato/core/plugins/searcher/__init__.py
This commit is contained in:
Ruud
2011-08-19 16:15:51 +02:00
37 changed files with 1183 additions and 81 deletions
+2 -1
View File
@@ -1,3 +1,4 @@
/settings.conf /settings.conf
/logs/*.log /logs/*.log
/_source/ /_source/
/_data/
+3
View File
@@ -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:
+30 -3
View File
@@ -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()
+3 -3
View File
@@ -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,
+3 -9
View File
@@ -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()
+6 -9
View File
@@ -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({
+3 -9
View File
@@ -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(",")]
+3 -11
View File
@@ -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')
+3 -11
View File
@@ -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)
+2 -2
View File
@@ -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
+9 -1
View File
@@ -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):
+3 -1
View File
@@ -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',
},
],
},
],
}]
+79
View File
@@ -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')
+1 -1
View File
@@ -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')
+5 -2
View File
@@ -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')
+1
View File
@@ -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));
} }
} }
+28
View File
@@ -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
+76
View File
@@ -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)
+75
View File
@@ -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")
+43
View File
@@ -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())
+53
View File
@@ -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
+32
View File
@@ -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
+59
View File
@@ -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
+45
View File
@@ -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
+51
View File
@@ -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
+399
View File
@@ -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,))
+59
View File
@@ -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()