1 from __future__ import unicode_literals, print_function
3 from datetime import datetime
6 from subprocess import PIPE, Popen
7 from tempfile import NamedTemporaryFile
9 from django.conf import settings
10 from django.core.files.base import ContentFile
11 from django.core.files.storage import FileSystemStorage
12 from django.db import models, transaction
13 from django.db.models.base import ModelBase
14 from django.utils.encoding import python_2_unicode_compatible
15 from django.utils.translation import ugettext_lazy as _
17 from dvcs.signals import post_commit, post_merge
18 from dvcs.storage import GzipFileSystemStorage
20 # default repository path; make a setting for it
21 REPO_PATH = os.path.join(settings.MEDIA_ROOT, 'dvcs')
22 repo = GzipFileSystemStorage(location=REPO_PATH)
25 @python_2_unicode_compatible
26 class Revision(models.Model):
28 A document revision. The "parent"
29 argument points to the version against which this change has been
30 recorded. Initial text will have a null parent.
32 Gzipped text of the document is stored in a file.
34 author = models.ForeignKey(settings.AUTH_USER_MODEL,
35 null=True, blank=True, verbose_name=_('author'))
36 author_name = models.CharField(_('author name'), max_length=128,
37 null=True, blank=True,
38 help_text=_("Used if author is not set.")
40 author_email = models.CharField(_('author email'), max_length=128,
41 null=True, blank=True,
42 help_text=_("Used if author is not set.")
44 # Any other author data?
45 # How do we identify an author?
47 parent = models.ForeignKey('self',
48 null=True, blank=True, default=None,
49 verbose_name=_('parent'),
50 related_name="children")
52 merge_parent = models.ForeignKey('self',
53 null=True, blank=True, default=None,
54 verbose_name=_('merge parent'),
55 related_name="merge_children")
57 description = models.TextField(_('description'), blank=True, default='')
58 created_at = models.DateTimeField(editable=False, db_index=True,
62 ordering = ('created_at',)
63 verbose_name = _("revision")
64 verbose_name_plural = _("revisions")
67 return "Id: %r, Parent %r, Data: %s" % (self.id, self.parent_id, self.get_text_path())
69 def get_text_path(self):
71 return re.sub(r'([0-9a-f]{2})([^\.])', r'\1/\2', '%x.gz' % self.pk)
75 def save_text(self, content):
76 return repo.save(self.get_text_path(), ContentFile(content.encode('utf-8')))
80 return "%s %s <%s>" % (
81 self.author.first_name,
82 self.author.last_name,
91 def create(cls, text, parent=None, merge_parent=None,
92 author=None, author_name=None, author_email=None,
97 '<dc:></dc:>', '').replace(
98 '<div class="img">', '<div>')
100 revision = cls.objects.create(
102 merge_parent=merge_parent,
104 author_name=author_name,
105 author_email=author_email,
106 description=description
108 revision.save_text(text)
111 def materialize(self):
112 f = repo.open(self.get_text_path())
113 text = f.read().decode('utf-8')
117 '<dc:></dc:>', '').replace(
118 '<div class="img">', '<div>')
121 def is_descendant_of(self, other):
125 self.parent is not None and (
126 self.parent.pk == other.pk or
127 self.parent.is_descendant_of(other)
130 self.merge_parent is not None and (
131 self.merge_parent.pk == other.pk or
132 self.merge_parent.is_descendant_of(other)
137 def get_common_ancestor_with(self, other):
138 # VERY naive approach.
139 if self.pk == other.pk:
141 if self.is_descendant_of(other):
143 if other.is_descendant_of(self):
146 if self.parent is not None:
147 parent_ca = self.parent.get_common_ancestor_with(other)
151 if self.merge_parent is not None:
152 merge_parent_ca = self.merge_parent.get_common_ancestor_with(other)
156 if parent_ca is None or parent_ca.created_at < merge_parent_ca.created_at:
157 return merge_parent_ca
161 def get_ancestors(self):
163 if self.parent is not None:
164 revs.add(self.parent)
165 revs.update(self.parent.get_ancestors())
166 if self.merge_parent is not None:
167 revs.add(self.merge_parent)
168 revs.update(self.merge_parent.get_ancestors())
171 @python_2_unicode_compatible
172 class Ref(models.Model):
173 """A reference pointing to a specific revision."""
175 revision = models.ForeignKey(Revision,
176 null=True, blank=True, default=None,
177 verbose_name=_('revision'),
178 help_text=_("The document's revision."),
182 return "ref:{0}->rev:{1}".format(self.id, self.revision_id)
184 def merge_text(self, base, local, remote):
185 """Override in subclass to have different kinds of merges."""
187 for f in local, base, remote:
188 temp = NamedTemporaryFile(delete=False)
191 files.append(temp.name)
192 p = Popen(['/usr/bin/diff3', '-mE', '-L', 'old', '-L', '', '-L', 'new'] + files, stdout=PIPE)
193 result, errs = p.communicate()
198 return result.decode('utf-8')
200 def merge_with(self, revision,
201 author=None, author_name=None, author_email=None,
202 description="Automatic merge."):
203 """Merges a given revision into the ref."""
204 if self.revision is None:
206 self.revision = revision
207 elif self.revision.pk == revision.pk or self.revision.is_descendant_of(revision):
210 elif revision.is_descendant_of(self.revision):
213 self.revision = revision
215 # Need to create a merge revision.
217 base = self.revision.get_common_ancestor_with(revision)
219 local_text = self.materialize().encode('utf-8')
220 base_text = base.materialize().encode('utf-8')
221 other_text = revision.materialize().encode('utf-8')
223 merge_text = self.merge_text(base_text, local_text, other_text)
225 merge_revision = Revision.create(
227 parent=self.revision,
228 merge_parent=revision,
230 author_name=author_name,
231 author_email=author_email,
232 description=description
234 self.revision = merge_revision
236 post_merge.send(sender=type(self), instance=self, fast_forward=fast_forward)
238 def materialize(self):
239 return self.revision.materialize() if self.revision is not None else ''
241 def commit(self, text, parent=False,
242 author=None, author_name=None, author_email=None,
244 """Creates a new revision and sets it as the ref.
246 This will automatically merge the commit into the main branch,
247 if parent is not document's head.
249 :param unicode text: new version of the document
250 :param base: parent revision (head, if not specified)
251 :type base: Revision or None
252 :param User author: the commiter
253 :param unicode author_name: commiter name (if ``author`` not specified)
254 :param unicode author_email: commiter e-mail (if ``author`` not specified)
258 # If parent revision not set explicitly, use your head.
259 parent = self.revision
261 # Warning: this will silently leave revs unreferenced.
262 rev = Revision.create(
265 author_name=author_name,
266 author_email=author_email,
267 description=description,
270 self.merge_with(rev, author=author, author_name=author_name,
271 author_email=author_email)
273 post_commit.send(sender=type(self), instance=self)
276 revs = self.revision.get_ancestors()
277 revs.add(self.revision)
278 return sorted(revs, key=lambda x: x.created_at)