convert: option to set date and time for svn commits
authorNikita Slyusarev <nslus@yandex-team.com>
Tue, 12 Jan 2021 00:11:16 +0300
changeset 46400 7525e77b5eac
parent 46399 1d6d1a15a963
child 46401 2aef69e8efbb
convert: option to set date and time for svn commits Converting to subversion repository is not preserving original commit dates as it may break some subversion functionality if commit dates are not monotonically increasing. This patch adds `convert.svn.dangerous-set-commit-dates` configuration option to change this behaviour and enable commit dates convertion for those who want to take risks. Subversion always uses commit dates with UTC timezone, so only timestamps are used. Test `test-convert-svn-sink.t` uses `svnxml.py` script to dump history of svn repositories. Atm the script is not printing `date` field from svn log. This patch changes this to allow checks on correctness of date and time convertion. Documentation is updated. Additional test case is added to test commit dates convertion. Differential Revision: https://phab.mercurial-scm.org/D9721
hgext/convert/__init__.py
hgext/convert/subversion.py
mercurial/configitems.py
tests/svnxml.py
tests/test-convert-svn-sink.t
tests/test-convert.t
--- a/hgext/convert/__init__.py	Fri Jan 15 23:58:41 2021 +0100
+++ b/hgext/convert/__init__.py	Tue Jan 12 00:11:16 2021 +0300
@@ -491,6 +491,22 @@
 
     :convert.skiptags: does not convert tags from the source repo to the target
         repo. The default is False.
+
+    Subversion Destination
+    ######################
+
+    Original commit dates are not preserved by default.
+
+    :convert.svn.dangerous-set-commit-dates: preserve original commit dates,
+        forcefully setting ``svn:date`` revision properties. This option is
+        DANGEROUS and may break some subversion functionality for the resulting
+        repository (e.g. filtering revisions with date ranges in ``svn log``),
+        as original commit dates are not guaranteed to be monotonically
+        increasing.
+
+    For commit dates setting to work destination repository must have
+    ``pre-revprop-change`` hook configured to allow setting of ``svn:date``
+    revision properties. See Subversion documentation for more details.
     """
     return convcmd.convert(ui, src, dest, revmapfile, **opts)
 
--- a/hgext/convert/subversion.py	Fri Jan 15 23:58:41 2021 +0100
+++ b/hgext/convert/subversion.py	Tue Jan 12 00:11:16 2021 +0300
@@ -97,6 +97,17 @@
         return s.decode(fsencoding).encode('utf-8')
 
 
+def formatsvndate(date):
+    return dateutil.datestr(date, b'%Y-%m-%dT%H:%M:%S.000000Z')
+
+
+def parsesvndate(s):
+    # Example SVN datetime. Includes microseconds.
+    # ISO-8601 conformant
+    # '2007-01-04T17:35:00.902377Z'
+    return dateutil.parsedate(s[:19] + b' UTC', [b'%Y-%m-%dT%H:%M:%S'])
+
+
 class SvnPathNotFound(Exception):
     pass
 
@@ -1158,12 +1169,7 @@
                     continue
                 paths.append((path, ent))
 
-            # Example SVN datetime. Includes microseconds.
-            # ISO-8601 conformant
-            # '2007-01-04T17:35:00.902377Z'
-            date = dateutil.parsedate(
-                date[:19] + b" UTC", [b"%Y-%m-%dT%H:%M:%S"]
-            )
+            date = parsesvndate(date)
             if self.ui.configbool(b'convert', b'localtimezone'):
                 date = makedatetimestamp(date[0])
 
@@ -1380,7 +1386,7 @@
         return logstream(stdout)
 
 
-pre_revprop_change = b'''#!/bin/sh
+pre_revprop_change_template = b'''#!/bin/sh
 
 REPOS="$1"
 REV="$2"
@@ -1388,15 +1394,26 @@
 PROPNAME="$4"
 ACTION="$5"
 
-if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
-if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
-if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
+%(rules)s
 
 echo "Changing prohibited revision property" >&2
 exit 1
 '''
 
 
+def gen_pre_revprop_change_hook(prop_actions_allowed):
+    rules = []
+    for action, propname in prop_actions_allowed:
+        rules.append(
+            (
+                b'if [ "$ACTION" = "%s" -a "$PROPNAME" = "%s" ]; '
+                b'then exit 0; fi'
+            )
+            % (action, propname)
+        )
+    return pre_revprop_change_template % {b'rules': b'\n'.join(rules)}
+
+
 class svn_sink(converter_sink, commandline):
     commit_re = re.compile(br'Committed revision (\d+).', re.M)
     uuid_re = re.compile(br'Repository UUID:\s*(\S+)', re.M)
@@ -1470,9 +1487,20 @@
             self.is_exec = None
 
         if created:
+            prop_actions_allowed = [
+                (b'M', b'svn:log'),
+                (b'A', b'hg:convert-branch'),
+                (b'A', b'hg:convert-rev'),
+            ]
+
+            if self.ui.configbool(
+                b'convert', b'svn.dangerous-set-commit-dates'
+            ):
+                prop_actions_allowed.append((b'M', b'svn:date'))
+
             hook = os.path.join(created, b'hooks', b'pre-revprop-change')
             fp = open(hook, b'wb')
-            fp.write(pre_revprop_change)
+            fp.write(gen_pre_revprop_change_hook(prop_actions_allowed))
             fp.close()
             util.setflags(hook, False, True)
 
@@ -1667,6 +1695,23 @@
                     revprop=True,
                     revision=rev,
                 )
+
+            if self.ui.configbool(
+                b'convert', b'svn.dangerous-set-commit-dates'
+            ):
+                # Subverson always uses UTC to represent date and time
+                date = dateutil.parsedate(commit.date)
+                date = (date[0], 0)
+
+                # The only way to set date and time for svn commit is to use propset after commit is done
+                self.run(
+                    b'propset',
+                    b'svn:date',
+                    formatsvndate(date),
+                    revprop=True,
+                    revision=rev,
+                )
+
             for parent in parents:
                 self.addchild(parent, rev)
             return self.revid(rev)
--- a/mercurial/configitems.py	Fri Jan 15 23:58:41 2021 +0100
+++ b/mercurial/configitems.py	Tue Jan 12 00:11:16 2021 +0300
@@ -570,6 +570,11 @@
     default=0,
 )
 coreconfigitem(
+    b'convert',
+    b'svn.dangerous-set-commit-dates',
+    default=False,
+)
+coreconfigitem(
     b'debug',
     b'dirstate.delaywrite',
     default=0,
--- a/tests/svnxml.py	Fri Jan 15 23:58:41 2021 +0100
+++ b/tests/svnxml.py	Tue Jan 12 00:11:16 2021 +0300
@@ -15,6 +15,7 @@
     e['revision'] = entry.getAttribute('revision')
     e['author'] = xmltext(entry.getElementsByTagName('author')[0])
     e['msg'] = xmltext(entry.getElementsByTagName('msg')[0])
+    e['date'] = xmltext(entry.getElementsByTagName('date')[0])
     e['paths'] = []
     paths = entry.getElementsByTagName('paths')
     if paths:
@@ -42,7 +43,7 @@
     except AttributeError:
         fp = sys.stdout
     for e in entries:
-        for k in ('revision', 'author', 'msg'):
+        for k in ('revision', 'author', 'date', 'msg'):
             fp.write(('%s: %s\n' % (k, e[k])).encode('utf-8'))
         for path, action, fpath, frev in sorted(e['paths']):
             frominfo = b''
--- a/tests/test-convert-svn-sink.t	Fri Jan 15 23:58:41 2021 +0100
+++ b/tests/test-convert-svn-sink.t	Tue Jan 12 00:11:16 2021 +0300
@@ -54,10 +54,12 @@
    2 2 test a
   revision: 2
   author: test
+  date: * (glob)
   msg: modify a file
    M /a
   revision: 1
   author: test
+  date: * (glob)
   msg: add a file
    A /a
    A /d1
@@ -95,6 +97,7 @@
    3 3 test b
   revision: 3
   author: test
+  date: * (glob)
   msg: rename a file
    D /a
    A /b (from /a@2)
@@ -131,6 +134,7 @@
    4 4 test c
   revision: 4
   author: test
+  date: * (glob)
   msg: copy a file
    A /c (from /b@3)
   $ ls a a-hg-wc
@@ -167,6 +171,7 @@
    5 5 test .
   revision: 5
   author: test
+  date: * (glob)
   msg: remove a file
    D /b
   $ ls a a-hg-wc
@@ -209,6 +214,7 @@
    6 6 test c
   revision: 6
   author: test
+  date: * (glob)
   msg: make a file executable
    M /c
 #if execbit
@@ -247,6 +253,7 @@
    8 8 test newlink
   revision: 8
   author: test
+  date: * (glob)
   msg: move symlink
    D /link
    A /newlink (from /link@7)
@@ -278,6 +285,7 @@
    7 7 test f
   revision: 7
   author: test
+  date: * (glob)
   msg: f
    D /c
    A /d
@@ -315,6 +323,7 @@
    1 1 test d1/a
   revision: 1
   author: test
+  date: * (glob)
   msg: add executable file in new directory
    A /d1
    A /d1/a
@@ -343,6 +352,7 @@
    2 2 test d2/a
   revision: 2
   author: test
+  date: * (glob)
   msg: copy file to new directory
    A /d2
    A /d2/a (from /d1/a@1)
@@ -416,21 +426,25 @@
    4 4 test right-2
   revision: 4
   author: test
+  date: * (glob)
   msg: merge
    A /right-1
    A /right-2
   revision: 3
   author: test
+  date: * (glob)
   msg: left-2
    M /b
    A /left-2
   revision: 2
   author: test
+  date: * (glob)
   msg: left-1
    M /b
    A /left-1
   revision: 1
   author: test
+  date: * (glob)
   msg: base
    A /b
 
@@ -459,10 +473,12 @@
    2 2 test .hgtags
   revision: 2
   author: test
+  date: * (glob)
   msg: Tagged as v1.0
    A /.hgtags
   revision: 1
   author: test
+  date: * (glob)
   msg: Add file a
    A /a
   $ rm -rf a a-hg a-hg-wc
@@ -494,10 +510,12 @@
    2 2 test exec
   revision: 2
   author: test
+  date: * (glob)
   msg: remove executable bit
    M /exec
   revision: 1
   author: test
+  date: * (glob)
   msg: create executable
    A /exec
   $ test ! -x a-hg-wc/exec
@@ -540,11 +558,77 @@
    2 2 test b
   revision: 2
   author: test
+  date: * (glob)
   msg: Another change
    A /b
   revision: 1
   author: test
+  date: * (glob)
   msg: Some change
    A /a
 
   $ rm -rf a a-hg a-hg-wc
+
+Commit dates convertion
+
+  $ hg init a
+
+  $ echo a >> a/a
+  $ hg add a
+  adding a/a
+  $ hg --cwd a ci -d '1 0' -A -m 'Change 1'
+
+  $ echo a >> a/a
+  $ hg --cwd a ci -d '2 0' -m 'Change 2'
+
+  $ echo a >> a/a
+  $ hg --cwd a ci -d '2 0' -m 'Change at the same time'
+
+  $ echo a >> a/a
+  $ hg --cwd a ci -d '1 0' -m 'Change in the past'
+
+  $ echo a >> a/a
+  $ hg --cwd a ci -d '3 0' -m 'Change in the future'
+
+  $ hg convert --config convert.svn.dangerous-set-commit-dates=true -d svn a
+  assuming destination a-hg
+  initializing svn repository 'a-hg'
+  initializing svn working copy 'a-hg-wc'
+  scanning source...
+  sorting...
+  converting...
+  4 Change 1
+  3 Change 2
+  2 Change at the same time
+  1 Change in the past
+  0 Change in the future
+  $ svnupanddisplay a-hg-wc 0
+   5 5 test .
+   5 5 test a
+  revision: 5
+  author: test
+  date: 1970-01-01T00:00:03.000000Z
+  msg: Change in the future
+   M /a
+  revision: 4
+  author: test
+  date: 1970-01-01T00:00:01.000000Z
+  msg: Change in the past
+   M /a
+  revision: 3
+  author: test
+  date: 1970-01-01T00:00:02.000000Z
+  msg: Change at the same time
+   M /a
+  revision: 2
+  author: test
+  date: 1970-01-01T00:00:02.000000Z
+  msg: Change 2
+   M /a
+  revision: 1
+  author: test
+  date: 1970-01-01T00:00:01.000000Z
+  msg: Change 1
+   A /a
+
+  $ rm -rf a a-hg a-hg-wc
--- a/tests/test-convert.t	Fri Jan 15 23:58:41 2021 +0100
+++ b/tests/test-convert.t	Tue Jan 12 00:11:16 2021 +0300
@@ -388,6 +388,23 @@
                     does not convert tags from the source repo to the target
                     repo. The default is False.
   
+      Subversion Destination
+      ######################
+  
+      Original commit dates are not preserved by default.
+  
+      convert.svn.dangerous-set-commit-dates
+                    preserve original commit dates, forcefully setting
+                    "svn:date" revision properties. This option is DANGEROUS and
+                    may break some subversion functionality for the resulting
+                    repository (e.g. filtering revisions with date ranges in
+                    "svn log"), as original commit dates are not guaranteed to
+                    be monotonically increasing.
+  
+      For commit dates setting to work destination repository must have "pre-
+      revprop-change" hook configured to allow setting of "svn:date" revision
+      properties. See Subversion documentation for more details.
+  
   options ([+] can be repeated):
   
    -s --source-type TYPE source repository type