[damned-lies] Abstract VCS commands inside specific repository classes
- From: Claude Paroz <claudep src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [damned-lies] Abstract VCS commands inside specific repository classes
- Date: Thu, 4 Oct 2018 16:32:19 +0000 (UTC)
commit 072465e5f89bcdc1f1de74e1e01e7afb6e41c317
Author: Claude Paroz <claude 2xlibre net>
Date: Sun Sep 30 21:22:08 2018 +0200
Abstract VCS commands inside specific repository classes
stats/models.py | 201 ++++++++-------------------------------------------
stats/repos.py | 171 +++++++++++++++++++++++++++++++++++++++++++
stats/tests/tests.py | 5 +-
stats/tests/utils.py | 2 +
4 files changed, 204 insertions(+), 175 deletions(-)
---
diff --git a/stats/models.py b/stats/models.py
index f82c3c22..3836ebc0 100644
--- a/stats/models.py
+++ b/stats/models.py
@@ -24,7 +24,7 @@ from django.db import models
from common.fields import DictionaryField, JSONField
from common.utils import is_site_admin, run_shell_command
-from stats import utils, signals
+from stats import repos, signals, utils
from stats.doap import update_doap_infos
from people.models import Person
from languages.models import Language
@@ -43,7 +43,6 @@ VCS_TYPE_CHOICES = (
('svn', 'Subversion'),
('git', 'Git'),
('hg', 'Mercurial'),
- ('bzr', 'Bazaar')
)
BRANCH_HEAD_NAMES = (
@@ -206,6 +205,11 @@ class Branch(models.Model):
def __str__(self):
return "%s (%s)" % (self.name, self.module)
+ @cached_property
+ def _repo(self):
+ repo_class = repos.RepoBase.repo_class_by_type(self.module.vcs_type)
+ return repo_class(self)
+
def clean(self):
if self.checkout_on_creation and not self.module.archived:
try:
@@ -227,20 +231,7 @@ class Branch(models.Model):
def delete_checkout(self):
# Remove the repo checkout
- if self.module.vcs_type in ('cvs', 'svn'):
- if os.access(self.co_path, os.W_OK):
- shutil.rmtree(self.co_path)
- elif self.module.vcs_type == 'git':
- wdir = self.co_path
- if os.access(str(wdir), os.W_OK):
- if self.is_head():
- shutil.rmtree(str(wdir))
- else:
- with ModuleLock(self.module):
- run_shell_command(['git', 'checkout', 'master'], cwd=wdir)
- run_shell_command(['git', 'branch', '-D', self.name], cwd=wdir)
- #To be implemented for hg/bzr
-
+ self._repo.remove()
# Remove the pot/po generated files
if os.access(str(self.output_dir('ui')), os.W_OK):
shutil.rmtree(str(self.output_dir('ui')))
@@ -297,16 +288,6 @@ class Branch(models.Model):
""" Return True if the branch only appears in 'archived' releases """
return bool(self.releases.filter(weight__gt=0).count())
- def get_vcs_url(self):
- if self.module.vcs_type in ('hg', 'git'):
- return self.module.vcs_root
- elif self.vcs_subpath:
- return utils.url_join(self.module.vcs_root, self.module.name, self.vcs_subpath)
- elif self.is_head():
- return utils.url_join(self.module.vcs_root, self.module.name, "trunk")
- else:
- return utils.url_join(self.module.vcs_root, self.module.name, "branches", self.name)
-
def is_vcs_readonly(self):
return 'ssh://' not in self.module.vcs_root and not self.module.vcs_root.startswith('git@')
@@ -536,148 +517,39 @@ class Branch(models.Model):
if self.is_head() and self.file_changed("%s.doap" % self.module.name):
update_doap_infos(self.module)
- def _exists(self):
- """ Determine if branch (self) already exists (i.e. already checked out) on local FS """
- if self.module.vcs_type == 'git':
- if not self.co_path.exists():
- return False
- command = "git branch | grep %s" % self.name
- status, output, errs = run_shell_command(command, cwd=self.co_path)
- return output != ""
- elif self.module.vcs_type == 'hg':
- return self.id != None and os.access(str(self.co_path), os.X_OK | os.W_OK)
- else:
- return os.access(str(self.co_path), os.X_OK | os.W_OK)
-
def checkout(self):
""" Do a checkout or an update of the VCS files """
- module_name = self.module.name
- vcs_type = self.module.vcs_type
- localroot = os.path.join(settings.SCRATCHDIR, vcs_type)
- modulepath = self.co_path
- scmroot = self.module.vcs_root
-
- os.makedirs(localroot, exist_ok=True)
- command_list = []
- if self._exists():
- command_list.extend(self.update_repo(execute=False))
- else:
- # Checkout
- vcs_path = self.get_vcs_url()
- if vcs_type in ('hg', 'git'):
- moduledir = self.module.name
- else:
- moduledir = self.module.name + "." + self.name
-
- if vcs_type == "cvs":
- command_list.append((localroot,
- ['cvs', '-d%s' % scmroot, '-z4', 'co', '-d%s' % moduledir, '-r%s' % self.name,
- module_name]))
- elif vcs_type == "svn":
- command_list.append((localroot, ['svn', 'co', '--non-interactive', vcs_path, moduledir]))
- elif vcs_type == "hg":
- command_list.append((localroot, ['hg', 'clone', vcs_path, moduledir]))
- command_list.append((modulepath, ['hg', 'update', self.name]))
- elif vcs_type == "git":
- # We are assuming here that master is the first branch created
- if self.name == "master":
- command_list.append((localroot, ['git', 'clone', vcs_path, moduledir]))
- command_list.append((modulepath, ['git', 'remote', 'update']))
- command_list.append((modulepath, ['git', 'checkout', self.name]))
- else:
- command_list.append((modulepath, ['git', 'pull']))
- command_list.append((modulepath, ['git', 'checkout', '--track', '-b', self.name,
'origin/%s' % self.name]))
- # check if there are any submodules and init & update them
- command_list.append((modulepath, "if [ -e .gitmodules ]; then git submodule update --init;
fi"))
- elif vcs_type == "bzr":
- command_list.append((localroot, ['bzr', 'co', '--lightweight', vcs_path, moduledir]))
-
- # Run command(s)
- logging.debug("Checking '%s.%s' out to '%s'..." % (module_name, self.name, modulepath))
- for working_dir, command in command_list:
- run_shell_command(command, raise_on_error=True, cwd=working_dir)
-
- def update_repo(self, execute=True):
+ logging.debug("Checking '%s.%s' out to '%s'…" % (self.module.name, self.name, self.co_path))
+ self._repo.checkout()
+
+ def update_repo(self):
"""
Update existing repository checkout.
- WARNING: if execute is True, the calling method should acquire a lock
+ WARNING: the calling method should acquire a lock
for the module to not mix checkouts in different threads/processes.
"""
- modulepath = self.co_path
- logging.debug("Updating '%s.%s' (in '%s')..." % (self.module.name, self.name, modulepath))
- command_list = []
- if self.module.vcs_type == "cvs":
- command_list.append((modulepath, ['cvs', '-z4', 'up', '-Pd']))
- elif self.module.vcs_type == "svn":
- command_list.append((modulepath, ['svn', 'up', '--non-interactive']))
- elif self.module.vcs_type == "hg":
- command_list.append((modulepath, ['hg', 'revert', '--all']))
- elif self.module.vcs_type == "git":
- # tester "git checkout %(branch)s && git clean -dfq && git pull origin/%(branch)s"
- command_list.append((modulepath, ['git', 'checkout', '-f', self.name]))
- command_list.append((modulepath, ['git', 'fetch']))
- command_list.append((modulepath, ['git', 'reset', '--hard', 'origin/%s' % self.name]))
- command_list.append((modulepath, ['git', 'clean', '-dfq']))
- # check if there are any submodules and init & update them
- command_list.append((modulepath, "if [ -e .gitmodules ]; then git submodule update --init; fi"))
- elif self.module.vcs_type == "bzr":
- command_list.append((modulepath, ['bzr', 'up']))
- if execute:
- for working_dir, command in command_list:
- run_shell_command(command, raise_on_error=True, cwd=working_dir)
- else:
- return command_list
+ logging.debug("Updating '%s.%s' (in '%s')..." % (self.module.name, self.name, self.co_path))
+ self._repo.update()
def commit_po(self, po_file, domain, language, author):
""" Commit the file 'po_file' in the branch VCS repository """
- vcs_type = self.module.vcs_type
- if vcs_type not in ("git",):
- raise NotImplementedError("Commit is not implemented for '%s'" % vcs_type)
-
dest_full_path, existing, linguas_path = domain.commit_info(self, language)
with ModuleLock(self.module):
self.update_repo()
- if vcs_type == "git":
- base_path = self.co_path
-
- # Copy file in repo
- shutil.copyfile(str(po_file), str(dest_full_path))
-
- # git add file.po
- run_shell_command(
- ['git', 'add', str(dest_full_path.relative_to(base_path))],
- raise_on_error=True, cwd=base_path
- )
- if linguas_path:
- # Add locale to LINGUAS
- utils.insert_locale_in_linguas(linguas_path, language.locale)
- run_shell_command(
- ['git', 'add', str(linguas_path.relative_to(base_path))],
- raise_on_error=True, cwd=base_path
- )
- if existing:
- msg = "Update %s translation" % language.name
- else:
- msg = "Add %s translation" % language.name
-
- commit_cmd = ['git', 'commit', '-m', msg]
- if author:
- commit_cmd.extend(['--author', author])
- run_shell_command(commit_cmd, raise_on_error=True, cwd=base_path)
- # git push
- try:
- run_shell_command(
- ['git', 'push', 'origin', self.name], raise_on_error=True, cwd=base_path)
- except OSError:
- # Revert the commit
- run_shell_command(
- ['git', 'reset', '--hard', 'origin/%s' % self.name], cwd=base_path)
- raise
- else:
- _, out, _ = run_shell_command(
- ['git', 'log', '-n1', '--format=oneline'], cwd=base_path)
- commit_hash = out.split()[0] if out else ''
+ # Copy file in repo
+ shutil.copyfile(str(po_file), str(dest_full_path))
+
+ files_to_commit = [dest_full_path.relative_to(self.co_path)]
+ if linguas_path:
+ # Add locale to LINGUAS
+ utils.insert_locale_in_linguas(linguas_path, language.locale)
+ files_to_commit.append(linguas_path.relative_to(self.co_path))
+ if existing:
+ msg = "Update %s translation" % language.name
+ else:
+ msg = "Add %s translation" % language.name
+ commit_hash = self._repo.commit_files(files_to_commit, msg, author=author)
# Finish by updating stats
if existing:
@@ -696,21 +568,10 @@ class Branch(models.Model):
Try to cherry-pick a branch commit `commit_hash` into master.
Return True if the cherry-pick succeeds, False otherwise.
"""
- if self.module.vcs_type != "git":
- raise NotImplementedError("Commit cherry-pick is not implemented for '%s'" %
self.module.vcs_type)
with ModuleLock(self.module):
self.update_repo()
- commit_dir = self.co_path
- result = run_shell_command(
- ['git', 'cherry-pick', '-x', commit_hash], cwd=commit_dir)
- if result[0] == utils.STATUS_OK:
- run_shell_command(
- ['git', 'push', 'origin', self.name], raise_on_error=True, cwd=commit_dir)
- return True
- else:
- # Revert
- run_shell_command(['git', 'cherry-pick', '--abort'], cwd=commit_dir)
- return False
+ success = self._repo.cherry_pick(commit_hash)
+ return success
DOMAIN_TYPE_CHOICES = (
@@ -1595,10 +1456,6 @@ class Statistics(models.Model):
stats['prc'] = 100*stats['translated']/stats['total']
return stats
- def vcs_path(self):
- """ Return the VCS path of file on remote vcs """
- return utils.url_join(self.branch.get_vcs_url(), self.domain.base_dir)
-
def vcs_web_path(self):
""" Return the Web interface path of file on remote vcs """
return utils.url_join(self.branch.get_vcs_web_url(), self.domain.base_dir)
diff --git a/stats/repos.py b/stats/repos.py
new file mode 100644
index 00000000..6913a98e
--- /dev/null
+++ b/stats/repos.py
@@ -0,0 +1,171 @@
+import os
+import shutil
+
+from common.utils import run_shell_command
+from stats.utils import STATUS_OK
+
+
+class RepoBase:
+ @classmethod
+ def repo_class_by_type(cls, tp):
+ return {'cvs': CVSRepo, 'svn': SVNRepo, 'git': GitRepo, 'hg': MercurialRepo}.get(tp)
+
+ def __init__(self, branch):
+ self.branch = branch
+
+ def exists():
+ return os.access(str(self.branch.co_path), os.X_OK | os.W_OK)
+
+ def checkout(self):
+ if self.exists():
+ self.update()
+ return
+ else:
+ self.init_checkout()
+
+ def init_checkout(self):
+ raise NotImplementedError
+
+ def update(self):
+ raise NotImplementedError
+
+ def commit_files(self, files):
+ raise NotImplementedError
+
+ def cherry_pick(self, commit_hash):
+ raise NotImplementedError
+
+ def remove(self):
+ raise NotImplementedError
+
+
+class GitRepo(RepoBase):
+ def exists(self):
+ if not self.branch.co_path.exists():
+ return False
+ command = "git branch | grep %s" % self.branch.name
+ status, output, errs = run_shell_command(command, cwd=self.branch.co_path)
+ return output != ""
+
+ def init_checkout(self):
+ # We are assuming here that master is the first branch created
+ if self.name == "master":
+ commands = [
+ ['git', 'clone', self.branch.module.vcs_root, self.branch.co_path.parent],
+ ['git', 'remote', 'update'],
+ ['git', 'checkout', self.branch.name],
+ ]
+ else:
+ commands = [
+ ['git', 'pull'],
+ ['git', 'checkout', '--track', '-b', self.branch.name, 'origin/%s' % self.branch.name],
+ ]
+ # check if there are any submodules and init & update them
+ commands.append("if [ -e .gitmodules ]; then git submodule update --init; fi")
+ for cmd in commands:
+ working_dir = self.branch.co_path.parent if 'clone' in cmd else self.branch.co_path
+ run_shell_command(command, raise_on_error=True, cwd=working_dir)
+
+ def update(self):
+ # test "git checkout %(branch)s && git clean -dfq && git pull origin/%(branch)s"?
+ commands = [
+ ['git', 'checkout', '-f', self.branch.name],
+ ['git', 'fetch'],
+ ['git', 'reset', '--hard', 'origin/%s' % self.branch.name],
+ ['git', 'clean', '-dfq'],
+ # check if there are any submodules and init & update them
+ "if [ -e .gitmodules ]; then git submodule update --init; fi",
+ ]
+ for cmd in commands:
+ run_shell_command(cmd, raise_on_error=True, cwd=self.branch.co_path)
+
+ def commit_files(self, files, message, author=None):
+ base_path = self.branch.co_path
+ for _file in files:
+ run_shell_command(['git', 'add', str(_file)], raise_on_error=True, cwd=base_path)
+ commit_cmd = ['git', 'commit', '-m', message]
+ if author:
+ commit_cmd.extend(['--author', author])
+ run_shell_command(commit_cmd, raise_on_error=True, cwd=base_path)
+ # git push
+ try:
+ run_shell_command(
+ ['git', 'push', 'origin', self.branch.name], raise_on_error=True, cwd=base_path
+ )
+ except OSError:
+ # Revert the commit
+ run_shell_command(['git', 'reset', '--hard', 'origin/%s' % self.branch.name], cwd=base_path)
+ raise
+ else:
+ _, out, _ = run_shell_command(['git', 'log', '-n1', '--format=oneline'], cwd=base_path)
+ commit_hash = out.split()[0] if out else ''
+ return commit_hash
+
+ def cherry_pick(self, commit_hash):
+ commit_dir = self.branch.co_path
+ result = run_shell_command(['git', 'cherry-pick', '-x', commit_hash], cwd=commit_dir)
+ if result[0] == STATUS_OK:
+ run_shell_command(
+ ['git', 'push', 'origin', self.branch.name], raise_on_error=True, cwd=commit_dir
+ )
+ return True
+ else:
+ # Revert
+ run_shell_command(['git', 'cherry-pick', '--abort'], cwd=commit_dir)
+ return False
+
+ def remove(self):
+ wdir = str(self.branch.co_path)
+ if os.access(wdir, os.W_OK):
+ if self.branch.name == 'master':
+ shutil.rmtree(wdir)
+ else:
+ run_shell_command(['git', 'checkout', 'master'], cwd=wdir)
+ run_shell_command(['git', 'branch', '-D', self.branch.name], cwd=wdir)
+
+
+class SVNRepo(RepoBase):
+ def init_checkout(self):
+ run_shell_command([
+ 'svn', 'co', '--non-interactive',
+ self.branch.get_vcs_url(), self.branch.module.name + "." + self.branch.name
+ ])
+
+ def update(self):
+ run_shell_command(['svn', 'up', '--non-interactive'], raise_on_error=True, cwd=self.branch.co_path)
+
+ def remove(self):
+ if os.access(str(self.branch.co_path), os.W_OK):
+ shutil.rmtree(str(self.branch.co_path))
+
+
+class CVSRepo(RepoBase):
+ def init_checkout(self):
+ run_shell_command([
+ 'cvs', '-d%s' % self.branch.module.vcs_root, '-z4', 'co',
+ '-d%s' % self.branch.module.name + "." + self.branch.name,
+ '-r%s' % self.branch.name, self.module.name
+ ], cwd=os.path.join(settings.SCRATCHDIR, self.branch.module.vcs_type))
+
+ def update(self):
+ run_shell_command(['cvs', '-z4', 'up', '-Pd'], raise_on_error=True, cwd=self.branch.co_path)
+
+ def remove(self):
+ if os.access(str(self.branch.co_path), os.W_OK):
+ shutil.rmtree(str(self.branch.co_path))
+
+
+class MercurialRepo(RepoBase):
+ def exists(self):
+ return self.branch.id != None and os.access(str(self.branch.co_path), os.X_OK | os.W_OK)
+
+ def init_checkout(self):
+ base_path = self.branch.co_path
+ run_shell_command(
+ ['hg', 'clone', base_path, self.module.name],
+ cwd=base_path.parent
+ )
+ run_shell_command(['hg', 'update', self.branch.name], cwd=base_path)
+
+ def update(self):
+ run_shell_command(['hg', 'revert', '--all'], raise_on_error=True, cwd=self.branch.co_path)
diff --git a/stats/tests/tests.py b/stats/tests/tests.py
index 48f3c86f..ec160aa4 100644
--- a/stats/tests/tests.py
+++ b/stats/tests/tests.py
@@ -99,7 +99,6 @@ class ModuleTestCase(TestCase):
def test_branch_methods(self):
self.assertTrue(self.branch.is_head())
- self.assertEqual(self.branch.get_vcs_url(), "https://gitlab.gnome.org/GNOME/gnome-hello.git")
self.assertEqual(self.branch.get_vcs_web_url(), "https://gitlab.gnome.org/GNOME/gnome-hello/")
def test_branch_domains(self):
@@ -175,9 +174,9 @@ class ModuleTestCase(TestCase):
@test_scratchdir
def test_branch_exists(self):
branch = Branch.objects.get(name='master', module__name='zenity')
- self.assertFalse(branch._exists())
+ self.assertFalse(branch._repo.exists())
branch = Branch.objects.get(name='master', module__name='gnome-hello')
- self.assertTrue(branch._exists())
+ self.assertTrue(branch._repo.exists())
@test_scratchdir
def test_delete_branch(self):
diff --git a/stats/tests/utils.py b/stats/tests/utils.py
index e523866d..0930f4b9 100644
--- a/stats/tests/utils.py
+++ b/stats/tests/utils.py
@@ -22,6 +22,8 @@ class patch_shell_command:
self.patcher1.start()
self.patcher2 = patch('stats.utils.run_shell_command', side_effect=self.mocked_run_shell_command)
self.patcher2.start()
+ self.patcher3 = patch('stats.repos.run_shell_command', side_effect=self.mocked_run_shell_command)
+ self.patcher3.start()
return self.cmds
def mocked_run_shell_command(self, cmd, *args, **kwargs):
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]