summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDmitry Bogatov <KAction@debian.org>2018-11-10 03:09:43 +0000
committerDmitry Bogatov <KAction@debian.org>2018-11-10 03:09:43 +0000
commit486f4254b69321ca468f4349c8f8384a651c03ae (patch)
tree0518772cc17a0754d7b22ba16486dd64b2419fae
New upstream version 1.20
-rw-r--r--COPYING27
-rw-r--r--FAQ.asc228
-rw-r--r--INSTALL22
-rw-r--r--Makefile66
-rw-r--r--NEWS173
-rw-r--r--README21
-rw-r--r--TODO6
-rw-r--r--control24
-rwxr-xr-xsrc2757
-rw-r--r--src.1311
-rw-r--r--src.asc313
-rwxr-xr-xsrctest1512
12 files changed, 5460 insertions, 0 deletions
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..0498485
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,27 @@
+ BSD LICENSE
+
+Copyright (c) 2015, Eric S. Raymond
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+1. Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+
+2. 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.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"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 THE COPYRIGHT
+HOLDER OR CONTRIBUTORS 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.
diff --git a/FAQ.asc b/FAQ.asc
new file mode 100644
index 0000000..bf376b7
--- /dev/null
+++ b/FAQ.asc
@@ -0,0 +1,228 @@
+= SRC FAQ =
+version 1.6
+
+[[why]]
+== Why SRC instead of $VCS? ==
+
+Most version control systems today are multi-user, multi-file, and
+multi-branch oriented. These are all good features and properties to
+have, but they neglect the need to maintain simple single-file
+documents, such as HOWTOs and FAQs, much like the very file you are
+reading now. There is even a good use-case for small programs and
+scripts. Do you presently keep your `~/bin` contents under version
+control? If not, consider using SRC for them.
+
+[[another-vcs]]
+== $VCS already does single-file version control, why another one? ==
+
+It is true, other VCSes already fulfill this simple criterion, SCCS
+and RCS being some of the earliest examples dating back to the 1970s
+and 1980s. While SCCS died off due to its proprietary nature,
+https://www.gnu.org/software/rcs[RCS] has kept a niche for itself
+precisely for single-file projects. In fact, SRC is built on top of
+RCS, rather than reimplementing all of the gritty master file details.
+
+The idea that spawned the development of SRC was that it would
+have these five properties:
+
+1. Only deals with single files. Use for HOWTOs, memoranda, scripts, etc.
+
+2. Allows multiple histories to live in the same directory without entanglement.
+
+3. Has a human-readable master-file representation - no binary blobs.
+
+4. Modern CLI user-interface. Commands familiar to Subversion, Hg, Git users.
+
+5. Integer sequential revision numbers a la Subversion.
+
+Notably, RCS itself fails on the latter two criteria. Designed both as
+an early attempt at VCS and for multi-user environments, the commands
+are awkward to deal with and it requires complicated processes of
+locking and unlocking files in order to edit and commit them. None of
+this is appropriate anymore. Modern DVCSes with a non-locking model
+have proven more effective even for multi-user projects, let alone
+single-user ones that will never have contention issues.
+
+Other projects to mold Mercurial and Git for a single-file purpose
+will at the _very_ least fail criteria #3 and #5, and often #4 as
+well.
+
+[[dvcs-obsoletion]]
+== Does SRC mean that $DVCS is obsolete? ==
+
+Absolutely not! SRC and DVCSes serve entirely opposite needs. SRC’s
+strength is precisely when there is neither need nor desire for
+collaboration or publishing features, when there is only a single file
+and a single author for a file. In fact, if your script grows into a
+full project in its own right, SRC has a `src fast-export` command that
+can be used to jump-start a DVCS repository with the entire existing
+history intact.
+
+SRC might make certain uses of DVCS obsolete, such as keeping
+individual documents tucked away in their own directories so that the
+DVCS (which usually has a special repository directory named like
+`.hg` or `.git`) can operate. Scripts to impose a single-file concept
+on top of these systems do not go <<another-vcs,far enough>> with
+respect to the reasons SRC exists.
+
+[[self-hosting]]
+== Is SRC used to develop itself? ==
+
+No. That would be silly. SRC is a real software project, with
+several files that do, and should, have joint history. It has
+a public Git repository.
+
+SRC has real users (that is, other than the author) since day
+two after it was announced, but not for projects like itself.
+
+[[bare-rcs]]
+== I still use plain RCS for single files. Why move to SRC? ==
+
+Because process friction matters, even to hackers as ancient as
+you and the author of SRC. Bare RCS's interface is pretty awful;
+a competently-designed UI can reduce that friction considerably.
+Try it and see.
+
+If you use RCS through Emacs's VC mode, you already have a pretty
+good wrapper around it...which, as it happens, the author of SRC
+also wrote. SRC has a couple of advantages over VC-RCS - running
+lockless, better scriptability, and ditching RCS's ugly revision
+numbering are the main ones.
+
+SRC is now a fully-supported VC-mode system in the Emacs development
+tree, so you can run SRC through Emacs and get the lockless operation
+and Subversion-style revision numbers.
+
+[[beginners-vcs]]
+== Is SRC a good system on which to learn about version control? ==
+
+*YES*! SRC is explicitly designed to have the bare-bones features and
+commands of a modern version control system. Keep in mind that SRC’s
+strength is single-file documents and projects. If you have loose
+scripts and documents not presently under any version control, SRC is
+a good candidate for playing around with them.
+
+If instead you have a large multi-file project, ease yourself into
+using a DVCS with simple commands, possibly even using SRC’s command
+set as a guideline for which ones to learn first. You will appreciate
+having actual changesets that span multiple files in this use
+case. http://mercurial.selenic.com/[Mercurial] and
+http://git-scm.com/[Git] are the most common, which means they are
+also easy to find help for.
+
+[[keywords]]
+== Does SRC have keyword expansion? ==
+
+No. When SRC commits a file with RCS on the backend, it uses `-kb` which
+explicitly disables all kind of expansion, and also allows arbitrary
+binary files to be stored. The SCCS backend always checks out files
+with -e, implying -k and no keyword expansion.
+
+Keyword expansion has, in general, not been well-accepted in the VCS
+world, most modern VCSes do not support it at all, and the author of
+SRC thinks it was a terrible idea. Do not even suggest this feature,
+it will not be implemented.
+
+[[missing-features]]
+== Does SRC have $FEATURE? ==
+
+If you don’t see it in the `src help` listing, probably not. You are
+certainly free to suggest features, but SRC is developed with extreme
+conservatism as to what features to implement or not. Remember,
+single-file, single-user, private VCS.
+
+Before requesting a feature, ask yourself whether it makes SRC more
+complicated, whether it really helps a single author or developer, and
+whether it really makes sense to deploy SRC for your use-case instead of
+a DVCS. These can all be hard questions, and if you are in doubt, you
+may go forth with your request, others may share their own opinions.
+
+SRC shines in its simplicity. Any individual feature is not
+necessarily against this, but too many can easily creep over into
+``too complicated'' territory.
+
+[[network]]
+== How well does SRC handle files over the network? ==
+
+The answer is either ``completely fine'' or ``not at all'', depending on
+what is being asked. :-)
+
+SRC makes no special provisions, it operates in the current working
+directory whether that is local storage, NFS, CIFS, sshfs, or any
+other kind of networking file system. As long as the directory tree is
+mounted on your system, SRC should be able to handle it.
+
+[[status-inconsistency]]
+== Why doesn’t src status display show the same letters as $VCS? ==
+
+Ah, yes, everybody's favorite bikeshedding topic.
+
+Consistency with other version control systems is an important way to
+reduce any kind of surprises while using SRC. Unfortunately, the
+single-letter codes used for statuses are not identical between VCSes
+and often conflict with each other over specific meanings. For
+example, *D* means 'deleted' in Subversion and Git, but Mercurial uses
+*R* for that same meaning. Git uses *R* to mean 'renamed', while
+Subversion uses it to mean 'replaced'.
+
+It is an unfortunate state of affairs. The development philosophy
+behind SRC is to keep it as un-innovative and unsurprising as
+possible, but since multiple VCSes in widespread use have not
+converged on the same meanings for single-letter status codes, SRC
+needs to settle on its own definitions that may differ from what you
+are used to.
+
+[[SCCS]]
+== Why did you bother with SCCS support? ==
+
+Because some hacks that are useless or marginal are way too funny not to
+do anyway. This one was *hilarious*...if you have a hacker's sense of
+humor. If you don't, never mind.
+
+[[not-a-joke]]
+== Um. This is all a joke, right? ==
+
+No, though the author admits he did laugh a lot while roughing out the
+original design. Resurrect RCS? Wrap it in a decent UI? _Really?_
+
+There's a significant amount of ha-ha-only-serious here. Laugh, but
+treat SRC as a lesson in several things. Unix minimalism. The virtue
+of re-use, even of technology as old as RCS. The effectiveness of a
+carefully crafted UI. The value of a conservative design with no
+surprises and no undue cleverness.
+
+[[reporting-bugs]]
+== How should I report bugs in SRC? ==
+
+This isn't really a FAQ, as nobody has asked it frequently enough; SRC
+is simple, carefully-tested code and bugs in it have been
+rare. Still...
+
+The first thing to do is pull the tip version from the project
+repository and verify that it still exhibits the bug. If it
+doesn't, you need read no further.
+
+When you a bug persists with the repository tip version, the author
+needs three things:
+
+1. An exact description of how you tickled the bug, which should at
+least include the SRC command or sequence of commands that produced
+it.
+
+2. A copy of the repository file(s) over which the bug was triggered.
+
+3. The output of "src version".
+
+Points for style if you send a tarball that unpacks into a
+directory containing relevant workfiles, their .src/RCS/SCCS
+subdirectory, a file of SRC commands to reproduce the bug (named
+'tickleme' or 'fooup' or something distinctive), and a README listing
+the "src version" output and circumstances.
+
+If you can supply all this information, expect bug resolution to
+be very rapid.
+
+[[ack]]
+== Acknowledgments ==
+
+Most of this FAQ was originally written by Mike Swanson, aka chungy.
diff --git a/INSTALL b/INSTALL
new file mode 100644
index 0000000..b020157
--- /dev/null
+++ b/INSTALL
@@ -0,0 +1,22 @@
+= Installing SRC =
+
+Have RCS or CSSC or both installed on your system. Have "python"
+available as a Python 2.7 or 3.x interpreter, x >= 3. Put
+src somewhere on your $PATH. That's all there is to it.
+
+(The SCCS support is limited, does not include tag or branch support,
+and not really recommended unless you have a specific need to work
+with legacy SCCS histories.)
+
+If you want to use the "visualize" command, you will need to install the
+graphviz package to get dot(1).
+
+There's a 'make install' production you can use that also installs the
+manual page. It will require root permissions.
+
+Doing 'make check' performs a comprehensive test of the software. You
+must have both RCS and CSSC installed for this to work. You must also have
+a Git user configuration with user.name and user.email set; this is
+required for the fast-export test.
+
+The FAQ has a section on how to report bugs.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..6737880
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,66 @@
+#
+# makefile for src
+#
+
+prefix?=/usr/local
+mandir?=/share/man
+target=$(DESTDIR)$(prefix)
+
+VERS=$(shell ./src version | sed -n -e "/src: /s///p")
+
+SOURCES = README INSTALL COPYING NEWS TODO src srctest src.asc FAQ.asc Makefile control
+
+all: src.1
+
+check: pylint
+ make pylint
+ ./srctest -b rcs -p python2
+ ./srctest -b sccs -p python2
+ ./srctest -b rcs -p python3
+ ./srctest -b sccs -p python3
+
+.SUFFIXES: .html .asc .txt .1
+
+# Requires asciidoc and xsltproc/docbook stylesheets.
+.asc.1:
+ a2x --doctype manpage --format manpage $<
+.asc.html:
+ a2x --doctype manpage --format xhtml -D . $<
+ rm -f docbook-xsl.css
+
+FAQ.html: FAQ.asc
+ asciidoc -a toc FAQ.asc
+
+clean:
+ rm -f *~ *.1 *.html *.tar.gz MANIFEST
+ rm -fr .rs* typescript test/typescript
+
+src-$(VERS).tar.gz: $(SOURCES) src.1
+ @ls $(SOURCES) src.1 | sed s:^:src-$(VERS)/: >MANIFEST
+ @(cd ..; ln -s src src-$(VERS))
+ (cd ..; tar -czf src/src-$(VERS).tar.gz `cat src/MANIFEST`)
+ @(cd ..; rm src-$(VERS))
+
+COMMON_PYLINT = --rcfile=/dev/null --reports=n \
+ --msg-template="{path}:{line}: [{msg_id}({symbol}), {obj}] {msg}" \
+ --dummy-variables-rgx='^_'
+PYLINTOPTS = "C0103,C0123,C0301,C0302,C0325,C0326,C0330,C0410,C1001,C0111,R0101,R0902,R0903,R0904,R0201,R0912,R0913,R0914,R0915,R1705,W0110,W0141,W0142,W0232,W0311,W0312,W0603,W0511,W0613,E1101,E1103"
+pylint:
+ @pylint $(COMMON_PYLINT) --disable=$(PYLINTOPTS) src
+
+version:
+ @echo $(VERS)
+
+dist: src-$(VERS).tar.gz
+
+release: src-$(VERS).tar.gz src.html FAQ.html
+ shipper version=$(VERS) | sh -e -x
+
+refresh: src.html FAQ.html
+ shipper -N -w version=$(VERS) | sh -e -x
+
+install: all
+ install -d "$(target)/bin"
+ install -d "$(target)$(mandir)/man1"
+ install src "$(target)/bin"
+ install -m644 src.1 "$(target)$(mandir)/man1"
diff --git a/NEWS b/NEWS
new file mode 100644
index 0000000..d83c7f9
--- /dev/null
+++ b/NEWS
@@ -0,0 +1,173 @@
+= src project news =
+
+1.20: 2018-10-28::
+ Add platform to "src version" output to make bug reports easier to generate.
+ Signal-harden backend command execution.
+
+1.19: 2018-10-27::
+ Filenames containing embedded spaces are handled properly.
+ The '--' token is interpreted correctly in cat commands.
+
+1.18: 2018-03-17::
+ Fix out-of-order 'revno' assignment when commits share same date.
+ 'list' & 'log' suppress RFC 822 headers inserted by 'fast-import'.
+ 'log' gained '-v' verbosity levels to show imported RFC 822 headers.
+ 'list' & 'log' show author date if in RFC 822 headers rather than committer.
+ Improved 'fast-import/export' round-trip fidelity using RFC 822 headers.
+
+1.17: 2017-11-14::
+ Show diff of changes to be committed when editing/amending commit comment.
+ 'src commit' no longer opens editor when file has not been changed.
+ 'src diff' and 'src log' accept '-b' & '-w' to ignore whitespace changes.
+ 'git log -p 1' shows "big bang" creation diff (previously no diff for r1).
+ Commands now operate on all managed files (as documented) when none given.
+ Colored output no longer crashes under Python 3.
+ 'src log -<n>' is alias for '-l <n>' a la 'git log -<n>'; ditto 'src list'.
+
+1.16: 2017-11-05::
+ Output of src diff & src log are colorized a la git when issuing to terminal.
+ 'src log' now accepts '-p' to emit patches a la 'git log -p'.
+
+1.15: 2017-10-30::
+ Fixes for fast-import, fast-export, and exec-bit propagation.
+
+1.14: 2017-10-17::
+ Slightly improved boilerplate commit message.
+ Fix for a minor command-parsing bug.
+
+1.13: 2017-03-26::
+ Improvement to tempfile handling; avoid wiring in /tmp.
+
+1.12: 2017-01-24::
+ Pylint cleanup, minor documentation fixes, and Python 3 port tweaks.
+
+1.11: 2016-02-23::
+ Now handles binary repository data cleanly under Python 3.
+ Version command reports versions for the underlying Python and back end.
+
+1.10: 2016-02-18::
+ Code now runs under either Python 2 or Python 3.
+ Restore (undocumented) add command for Emacs VC compatibility.
+
+1.9: 2016-02-15::
+ Fix status-computation bug introduced in 1.8.
+ SCCS parsing can no longer be fooled by comment lines resembing delta headers.
+
+1.8: 2016-02-14::
+ Stamp files are no longer used; all SRC state is carried in the master itself.
+
+1.7: 2016-02-10::
+ New 'visualize' command makes a DOT/graphviz visualization of repo structure.
+ It is now possible to range-restrict a tag or branch listing.
+
+1.6: 2016-02-08::
+ Improved prs log parsing in SCCS back end allows blank lines in comments.
+ SCCS regression tests now really work (previously a false positive).
+
+1.5: 2016-02-07::
+ Bugfixes for SCCS support. It now has its own regression test.
+ Documentation and FAQ update for the new back end.
+
+1.4: 2016-02-05::
+ Basic SCCS support - no branches or tags, limited diffs.
+ The diff command no longer leaks native commit IDs onto the screen.
+ In fast-export, properly sanitize RCS branch names illegal for git.
+
+1.3: 2016-02-04::
+ Make SRC able to drive RCS v5.7 (previously needed v5.8 or later).
+ If you change nothing in the template commit during edit, commit is canceled.
+
+1.2: 2016-01-29::
+ Documentation improvements based on user feedback.
+
+1.1: 2016-01-27::
+ Avoid upchucking on status A files if we happen to go through a modify check.
+ Add regression test for commit-comment editing.
+ Force binary I/O - a step towards Python 3 porting.
+
+1.0: 2016-01-26::
+ Now hosted on gitlab.
+ Fix for Tom Willemse's multi-file commit bug.
+
+0.19: 2015-04-02::
+ A pylint audit caught two unbound variables in odd cases.
+
+0.18: 2014-12-23::
+ Reversed range expressons are now supported.
+ In list and log, explicit ranges are no longer reversed.
+
+0.17: 2014-12-19::
+ Undocumented 'add' command abolished; Emacs VC will use 'commit -a' instead.
+
+0.16: 2014-12-18::
+ Allow -- as an argument ender in src diff.
+ Changes to a workfile's x bits are propagated to its master on checkin.
+
+0.15: 2014-12-08::
+ Deal gracefully with directories passed as arguments.
+
+0.14: 2014-11-29::
+ Fixed bugs affecting argument parsing in the presence of numeric filenames.
+
+0.13: 2014-11-24::
+ Fixed bug that caused spurious modified status after commit.
+
+0.12: 2014-11-21::
+ Log command shows the branch in the header line for each revision.
+ List and log command now have an -l option to play nice with Emacs VC.
+
+0.11: 2014-11-19::
+ File-not-modified status character changed to '=' to match Mercurial.
+ Fixed-width cutlines and list -f option support Emacs VC mode.
+
+0.10: 2014-11-19::
+ Modified check is now done by content, not modification date.
+
+0.9: 2014-11-16::
+ SRC is now feature-complete as planned.
+ Branch deletion is implemented and tested.
+ 'src rename' now renames tags and branches - no longer an alias for 'move'.
+ In tag and branch, a 'create' or '-c' modifier is now required to make tags.
+
+0.8: 2014-11-14::
+ A branch label names the tip revision of its branch.
+ src log and src list skip ignored and unregistered files in arguments.
+ The embedded help has been enriched and now features multiple topics.
+ ! works to negate patterns in ignore files.
+ src status -a forces status display of all files, including I and ?.
+
+0.7: 2014-11-13::
+ Bugfix release: fix initial file commit. There was a bug in my tests...
+
+0.6: 2014-11-12::
+ Useful boilerplate in commit and amend message buffers.
+ Tag names for revisions work; so does tag renaming.
+ Branch-sensitive traversal with .. is working.
+ Fixed yet another fast-export bug.
+
+0.5: 2014-11-11::
+ Removed src add. The first src commit to a file adds it.
+ Branching is mostly working - branch delete isn't implemented,
+ Tag and branch names to identify revisions don't work yet.
+ 'U' (unmodified) becomes '.' in src status listings.
+ src fast-export was broken in 0.4, it's fixed now.
+ Added src version.
+
+0.4: 2014-11-10::
+ Improvements to src fast-import.
+ src commit now has an -e option to force editing after -m or -f.
+ There is now a "src amend".
+
+0.3: 2014-11-09::
+ There is now a "src status" command, and .srcignore affects it.
+ src fast-import is implemented (by callout to rcs-fast-import).
+
+0.2: 2014-11-08::
+ There is a public repository on Gitorious
+ All the initially projected commands except branch are implemented.
+ The bug that caused failures with vi has been fixed.
+ ".." is a legal range separator as well as "-".
+ There is a regression-test suite, and more documentation.
+
+0.1: 2014-11-07::
+ Initial proof-of-concept release.
diff --git a/README b/README
new file mode 100644
index 0000000..c6d7e5e
--- /dev/null
+++ b/README
@@ -0,0 +1,21 @@
+ Simple Revision Control
+
+The venerable RCS (Revision Control System) has survived into the era
+of distributed version control because it fills a niche: sometimes you
+only *want* to track changes in single files at a time - for example,
+if you have a directory full of documents with separate histories.
+
+SRC (Simple Revision Control) is RCS, reloaded. It remains
+determinedly file-oriented and doesn't even track the committer of a
+change (because that's always you), but incorporates the design and
+user-interface lessons of modern version-control systems. It features
+sequential revision numbers, lockless operation, embedded command
+help, and a command set that will seem familiar to users of
+Subversion, Mercurial, and Git.
+
+See INSTALL for the (very simple) installation instructions.
+
+Run 'make check' for the regression test.
+
+ Eric S. Raymond
+ November 2014
diff --git a/TODO b/TODO
new file mode 100644
index 0000000..d2acaf7
--- /dev/null
+++ b/TODO
@@ -0,0 +1,6 @@
+ SRC TODO list
+
+* Eliminate 'L' status, turning it to '=' and automatically checking
+ out the file when required.
+
+
diff --git a/control b/control
new file mode 100644
index 0000000..1e3f077
--- /dev/null
+++ b/control
@@ -0,0 +1,24 @@
+# This is not a real Debian control file
+# It's project metadata for the shipper tool
+
+Package: src
+
+Description: Simple Revision Control
+ is RCS/SCCS reloaded with a modern UI, designed to manage single-file solo
+ projects kept more than one to a directory. Use it for FAQs, ~/bin
+ directories, config files, and the like. Features integer sequential
+ revision numbers, a command set that will seem familiar to
+ Subversion/Git/hg users, and no binary blobs anywhere.
+
+Homepage: http://www.catb.org/~esr/src
+
+XBS-Repository-URL: https://gitlab.com/esr/src
+
+XBS-HTML-Target: index.html
+
+XBS-Logo: src-logo.png
+
+#XBS-Project-Tags: RCS, SCCS, version control
+
+XBS-VC-Tag-Template: %(version)s
+
diff --git a/src b/src
new file mode 100755
index 0000000..08911ae
--- /dev/null
+++ b/src
@@ -0,0 +1,2757 @@
+#!/usr/bin/env python
+#
+# SRC - simple revision control.
+#
+# Things to know before hacking this:
+#
+# All the code outside the RCS and SCCS classes (and the RevisionMixin
+# helper) is intended to be generic to any file-oriented VCS.
+#
+# SRC and RCS/SCCS have different goals in managing locks. RCS/SCCS
+# wants to keep the workfile read-only except when it's explicitly
+# checked out, SRC wants to leave it writeable all the time. Thus,
+# the checkin sequence is "release lock; check in; assert lock". If
+# this seems confusing, it's because in RCS/SCCS terminology, locked is
+# writeable and unlocked is read-only.
+#
+# Despite appearances, this code does not actually use RCS locks (and
+# sets locking to non-strict). That just happens to be a handy way to
+# record which revision the user last checked out, which is significant
+# for future checkouts and for branching.
+#
+# With sufficient cleverness it would be possible to go in a different
+# direction - leave the master unlocked at all times except during during
+# the commit sequence, intervening with a chmod to turn off write protection
+# on the workfile when RCS/SCCS would normally turn it on. I had this as
+# a to-do for a long time but have abandoned the concept; fighting RCS/SCCS's
+# notion of when the workfile ought to be write-locked seems too likely to
+# lead to weird bugs in unexpected situations.
+#
+# In an ideal world, we'd get rid of A status. The SCCS back end doesn't
+# have it, because the only way to register new content is by "sccs add"
+# on an already existing file and that creates a new SCCS master with
+# the content checked in as the first commit. RCS ci works that way as
+# well. On the other hand, if you use rcs -i foo it creates a master foo,v
+# but does *not* stuff it with the content of any corresponding foo. This
+# is what A status means.
+#
+# This code uses magic tags with a 0 in the second-to-last slot to
+# designate branches. It's the same format as CVS sticky tags, for
+# the same reason. They need to be distinguishable from regular tags
+# pointing at revisions, and this way the code to transform the sticky
+# tag into the branch name is as simple as possible.
+#
+# Top of the list of things that people will bikeshed about is the
+# letter codes returned by 'src status'. Different VCSes have
+# conflicting ideas about this. The universal ones are 'A' = Added,
+# 'M' = Modified, and '?' = Untracked. Here's a table of the ones
+# in dispute. Entries with '-' mean the VCS does not have a closely
+# corresponding status.
+#
+# git hg svn src
+# Unmodified ' ' '=' ' ' '='
+# Renamed 'R' - - -
+# Deleted 'D' 'R' 'D' -
+# Copied 'C' - - -
+# Ignored '!' 'I' 'I' 'I'
+# Updated/unmerged 'U' - - -
+# Missing - '!' '!' '!'
+# Locked - - 'L' 'L'
+#
+# (hg used to use 'C' as the code for unmodified status.)
+#
+# This is a bit oversimplified; it is meant not as a technical
+# comparison but rather to illustrate how bad the letter collisions are.
+# SRC follows the majority except for absolutely *not* using a space as
+# a status code; this makes the reports too hard to machine-parse.
+#
+# SPDX-License-Identifier: BSD-2-Clause
+
+# This code runs under both Python 2 and Python 3.
+# Preserve this property!
+
+from __future__ import print_function
+
+import sys, os, subprocess, datetime, time, calendar, stat, glob
+import shutil, difflib, cgi, json, io, re, signal
+import tempfile, email.utils
+try:
+ import curses
+except ImportError:
+ pass
+
+version="1.20"
+
+CENTURY = "20" # A Y2.1K problem, but only for SCCS.
+
+# General notes on Python 2/3 compatibility:
+#
+# SRC uses the following strategy to allow it to run on both Python 2
+# and Python 3:
+#
+# * Use binary I/O to read/write data from/to files and subprocesses;
+# where the exact bytes are important (such as in checking for
+# modified files), use the binary data directly.
+#
+# * Use latin-1 encoding to transform binary data to/from Unicode when
+# necessary for operations where Python 3 expects Unicode; this will
+# ensure that bytes 0x80..0xff are passed through and not clobbered.
+# The polystr and polybytes functions are used to do this so that when
+# running on Python 2, the byte string data is used unchanged.
+#
+# * Construct custom stdin, stdout, and stderr streams when running
+# on Python 3 that force ASCII encoding, and wrap them around the
+# underlying binary buffers (in Python 2, the streams are binary and
+# are used unchanged); this ensures that the same transformation is
+# done on data from/to the standard streams, as is done on binary data
+# from/to files and subprocesses; the make_std_wrapper function does
+# this. Without this change, 0x80..0xff written to stdout will be
+# garbled in unpredictable ways.
+
+master_encoding = 'latin-1'
+
+if str is bytes: # Python 2
+
+ polystr = str
+ polybytes = bytes
+
+else: # Python 3
+
+ def polystr(obj):
+ "Polymorphic string factory function"
+ # This is something of a hack: on Python 2, bytes is an alias
+ # for str, so this ends up just giving a str back for all
+ # inputs; but on Python 3, if fed a byte string, it decodes it
+ # to Unicode using the specified master encoding, which should
+ # be either 'ascii' if you're sure all data being handled will
+ # be ASCII data, or 'latin-1' otherwise; this ensures that the
+ # original bytes can be recovered by re-encoding.
+ if isinstance(obj, str):
+ return obj
+ if not isinstance(obj, bytes):
+ return str(obj)
+ return str(obj, encoding=master_encoding)
+
+ def polybytes(s):
+ "Polymorphic string encoding function"
+ # This is the reverse of the above hack; on Python 2 it returns
+ # all strings unchanged, but on Python 3 it encodes Unicode
+ # strings back to bytes using the specified master encoding.
+ if isinstance(s, bytes):
+ return s
+ if not isinstance(s, str):
+ return bytes(s)
+ return bytes(s, encoding=master_encoding)
+
+ def make_std_wrapper(stream):
+ "Standard input/output wrapper factory function"
+ # This ensures that the encoding of standard output and standard
+ # error on Python 3 matches the master encoding we use to turn
+ # bytes to Unicode in polystr above.
+ return io.TextIOWrapper(stream.buffer, encoding=master_encoding, newline="\n")
+
+ sys.stdin = make_std_wrapper(sys.stdin)
+ sys.stdout = make_std_wrapper(sys.stdout)
+ sys.stderr = make_std_wrapper(sys.stderr)
+
+# Note: Avoid using unbolded blue (poor luminance contrast with black
+# terminal-emulator background) or bolded yellow (poor contrast with
+# white background.)
+RESET = BOLD = ''
+CBLACK = CBLUE = CGREEN = CCYAN = CRED = CMAGENTA = CYELLOW = CWHITE = ''
+
+def init_colors():
+ curses.setupterm()
+ global RESET, BOLD
+ RESET = polystr(curses.tigetstr('sgr0')) or ''
+ BOLD = polystr(curses.tigetstr('bold')) or ''
+ colors = {'setaf': [0, 4, 2, 6, 1, 5, 3, 7], # ANSI colors
+ 'setf': [0, 1, 2, 3, 4, 5, 6, 7]} # legacy colors
+ for (k, v) in colors.items():
+ x = curses.tigetstr(k)
+ if x:
+ for (i, c) in enumerate(['black', 'blue', 'green', 'cyan', 'red',
+ 'magenta', 'yellow', 'white']):
+ globals()['C' + c.upper()] = polystr(curses.tparm(x, v[i]))
+ break
+
+if 'curses' in sys.modules and sys.stdout.isatty():
+ try:
+ init_colors()
+ except (curses.error, AttributeError):
+ pass
+
+def rfc3339(t):
+ "RFC3339 string from Unix time."
+ return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(t))
+
+def announce(msg):
+ sys.stdout.write("src: " + msg + "\n")
+
+def croak(msg):
+ sys.stdout.flush()
+ sys.stderr.write("src: " + msg + "\n")
+ sys.exit(1)
+
+debug = 0
+DEBUG_COMMANDS = 1 # Show commands as they are executed
+DEBUG_SEQUENCE = 2 # Sequence debugging
+DEBUG_PARSE = 3 # Debug logfile parse
+
+quiet = False # option -q: run more quietly
+pseudotime = False # option -T: artificial clock for regression testing
+
+class popen_or_die:
+ "Read or write from a subordinate process."
+ def __init__(self, command, legend="", mode="r"):
+ assert mode in ("r", "w")
+ self.command = command
+ self.legend = legend
+ self.mode = mode
+ # Pipe to the correct streams depending on the chosen mode
+ self.stdin = (subprocess.PIPE if mode == "w" else None)
+ self.stdout = (subprocess.PIPE if mode == "r" else None)
+ self.stderr = (subprocess.STDOUT if mode == "r" else None)
+ if self.legend:
+ self.legend = " " + self.legend
+ self.fp = None
+ def __enter__(self):
+ if debug >= DEBUG_COMMANDS:
+ if self.mode == "r":
+ sys.stderr.write("%s: reading from '%s'%s\n" % (rfc3339(time.time()), self.command, self.legend))
+ else:
+ sys.stderr.write("%s: writing to '%s'%s\n" % (rfc3339(time.time()), self.command, self.legend))
+ try:
+ # The I/O streams for the subprocess are always bytes; this
+ # is what we want for some operations, but we will need
+ # to decode to Unicode for others to work in Python 3, as
+ # explained in the general notes.
+ self.fp = subprocess.Popen(self.command, shell=True,
+ stdin=self.stdin, stdout=self.stdout, stderr=self.stderr)
+ # The Python documentation recommends using communicate() to
+ # avoid deadlocks, but this doesn't allow fine control over
+ # reading the data; since we are not trying to both read
+ # from and write to the same process, this should be OK.
+ return self.fp.stdout if self.mode == "r" else self.fp.stdin
+ except (OSError, IOError) as oe:
+ croak("execution of %s%s failed: %s" \
+ % (self.command, self.legend, oe))
+ def __exit__(self, extype, value, traceback_unused):
+ if extype:
+ if debug > 0:
+ raise extype(value)
+ croak("fatal exception in popen_or_die.")
+ if self.fp.stdout is not None:
+ # This avoids a deadlock in wait() below if the OS pipe
+ # buffer was filled because we didn't read all of the data
+ # before exiting the context mgr (shouldn't happen but this
+ # makes sure).
+ self.fp.stdout.read()
+ self.fp.wait()
+ if self.fp.returncode != 0:
+ croak("%s%s returned error." % (self.command, self.legend))
+ return False
+
+def screenwidth():
+ "Return the current width of the terminal window."
+ width = 73
+ if "COLUMNS" in os.environ:
+ return int(os.environ["COLUMNS"])
+ if sys.stdin.isatty():
+ with popen_or_die('stty size', "rb") as tp:
+ # stty returns 0,0 inside Emacs
+ width = int(tp.read().split()[1]) or width
+ return width
+
+WIDTH = screenwidth() - 1
+
+def is_history(arg):
+ "Are we looking at a history file?"
+ for vcsb in backends:
+ if vcsb.is_history(arg):
+ return True
+ return False
+
+def modified(workfile, history=None):
+ "Has the workfile been modified since it was checked out?"
+ # Alas, we can't rely on modification times; it was tried, and
+ # os.utime() is flaky from Python - sometimes has no effect. Where
+ # the bug is - Python, glibc, kernel - is unknown. Even if we
+ # could, it's nice to catch the case where an edit was undone.
+ #
+ # If no revisions, bail out
+ if not backend.has_revisions(workfile):
+ return False
+ if history is None:
+ history = History(workfile)
+ with backend.lifter(workfile):
+ with backend.cat(workfile, history.current().native) as bstream:
+ base_content = bstream.read() # this data will be binary
+ with open(workfile, "rb") as fp:
+ workfile_content = fp.read()
+ # This comparison uses the binary data for maximum accuracy
+ return base_content != workfile_content
+
+def do_or_die(dcmd, legend="", mute=True, missing=None):
+ "Either execute a command or die."
+ if legend:
+ legend = " " + legend
+ if debug == 0 and mute:
+ muteme = " >/dev/null 2>&1"
+ else:
+ muteme = ""
+ if debug >= DEBUG_COMMANDS:
+ sys.stderr.write("executing '%s'%s\n" % (dcmd, legend))
+ try:
+ retcode = subprocess.call("(" + dcmd + ")" + muteme, shell=True)
+ if retcode < 0:
+ croak("%s was terminated by signal %d." % (repr(dcmd), -retcode))
+ elif retcode != 0:
+ errmsg = "%s returned %d." % (repr(dcmd), retcode)
+ if retcode == 127:
+ if missing is None:
+ missing = backend.__class__.__name__
+ errmsg += "\nYou probably need to install %s." % missing
+ croak(errmsg)
+ except (OSError, IOError) as e:
+ croak("execution of %s%s failed: %s" % (repr(dcmd), legend, e))
+
+def capture_or_die(command):
+ "Run a specified command, capturing the output."
+ if debug >= DEBUG_COMMANDS:
+ sys.stderr.write("%s: capturing %s\n" % (rfc3339(time.time()), command))
+ try:
+ # This will return binary data
+ content = subprocess.check_output(command, shell=True)
+ except (subprocess.CalledProcessError, OSError) as oe:
+ croak("execution of %s failed: %s" % (repr(command), oe))
+ if debug >= DEBUG_COMMANDS:
+ sys.stderr.write(polystr(content))
+ return content
+
+class HistoryEntry:
+ "Capture the state of a native revision item in the log."
+ def __init__(self, history):
+ self.history = history
+ self.revno = None
+ self.native = None # magic cookie only interpreted by back end
+ self.headers = None
+ self.log = ""
+ self.date = None
+ self.parent = None # Another HistoryEntry
+ self.child = None # Another HistoryEntry
+ self.branches = set([])
+ self.branch = None
+ def selected(self):
+ return self == self.history.current()
+ def getdate(self, who):
+ if self.headers and not pseudotime:
+ return self.headers.get(who + "-date") or self.date
+ return self.date
+ def unixtime(self, who):
+ date = self.getdate(who)
+ try:
+ t = calendar.timegm(time.strptime(date, "%Y-%m-%dT%H:%M:%SZ"))
+ offset = 0
+ if self.headers and not pseudotime:
+ offset = self.headers.get(who + "-date-offset") or 0
+ return t, offset
+ except (TypeError, ValueError):
+ croak("garbled date %s" % date)
+ def __str__(self):
+ return "<%s = %s>" % (self.revno, self.native)
+
+def registered(workfile):
+ "Is this a workfile for a registered history?"
+ return os.path.exists(backend.history(workfile))
+
+class History:
+ "Encapsulate a revision list and some methods on it"
+ def __init__(self, name):
+ self.name = name
+ self.revlist = []
+ self.symbols = {}
+ self.branch = "trunk"
+ self.lockrevs = []
+ self.description = ""
+ self.annotations = {}
+ if not registered(self.name):
+ croak("%s is not registered" % self.name)
+ self.by_revno_d = {}
+ self.by_native_d = {}
+ self.branches = set([])
+ backend.parse(self)
+ self.lift_headers()
+ self.normalize_header_dates()
+ def build_indices(self):
+ for item in self.revlist:
+ self.by_revno_d[item.revno] = item
+ self.by_native_d[item.native] = item
+ for item in self.revlist:
+ item.parent = self.by_native_d.get(backend.pred(item.native))
+ item.child = self.by_native_d.get(backend.succ(item.native))
+ if item.parent and item.parent.child != item:
+ item.parent.branches.add(item)
+ #sys.stderr.write("Symbols: %s\n" % self.symbols)
+ #for item in self.revlist:
+ # sys.stderr.write("Item %s\n" % item)
+ #sys.stderr.write("By revision: %s\n" % self.by_revno_d.keys())
+ if self.revlist:
+ for (name, rev) in list(self.symbols.items()):
+ if backend.isbranch(rev):
+ base = backend.branch_to_base(rev, self)
+ tip = backend.branch_to_tip(rev, self)
+ while True:
+ self.by_native_d[base].branch = name
+ if base == tip:
+ break
+ base = backend.succ(base)
+ # Digest JSON from the description field
+ if not self.description.strip():
+ self.annotations = {}
+ else:
+ try:
+ # The JSON loader returns Unicode, so this is one place
+ # where internal data will be Unicode even in Python 2;
+ # since ASCII data will be inter-converted between byte
+ # string and Unicode whenever needed in Python 2, this
+ # is not an issue.
+ self.annotations = json.loads(self.description.strip())
+ except ValueError as _e:
+ croak("legacy data %s in description field" % self.description)
+ def lift_headers(self):
+ valid = ('author', 'author-date', 'committer', 'committer-date',
+ 'mark', 'parents')
+ for item in self.revlist:
+ headers = {}
+ i = 0
+ while True:
+ n = item.log.find('\n', i)
+ if n < 0:
+ break
+ header = item.log[i:n].split(':', 1)
+ if len(header) != 2:
+ break
+ key = header[0].lower()
+ if key not in valid:
+ break
+ headers[key] = header[1].strip()
+ i = n + 1
+ # eat blank line between headers and body
+ while item.log[i] == '\n':
+ i += 1
+ item.log = item.log[i:]
+ if headers:
+ item.headers = headers
+ def normalize_header_dates(self):
+ for item in self.revlist:
+ if item.headers:
+ for k in tuple(item.headers):
+ if k.endswith("-date"):
+ d = email.utils.parsedate_tz(item.headers[k])
+ if d:
+ u = email.utils.mktime_tz(d)
+ u = datetime.datetime.utcfromtimestamp(u)
+ item.headers[k] = u.isoformat() + "Z"
+ item.headers[k + "-offset"] = d[9] if d[9] else 0
+ def __len__(self):
+ return len(self.revlist)
+ def current(self):
+ "Return the revision currently checked out."
+ # Yes, this looks weird. The idea is: Try to return the locked
+ # revision. If that blows up, try to return the tip revision of
+ # the current branch. If that blows up, return None.
+ try:
+ return self.by_native_d[self.lockrevs[0]]
+ except (IndexError, KeyError):
+ try:
+ return self.by_native_d[backend.branch_to_tip(self.symbols[self.branch], self)]
+ except (IndexError, KeyError):
+ return None
+ def current_branch(self, backwards=False):
+ "Return a list of items that are descendants or ancestors of current."
+ if debug >= DEBUG_SEQUENCE:
+ sys.stdout.write(("current_branch(%s)" % self.current()))
+ selection = []
+ p = self.current()
+ if p is not None:
+ selection = [p]
+ while True:
+ if p.parent is None:
+ break
+ else:
+ p = p.parent
+ selection = [p] + selection
+ s = self.current()
+ while True:
+ s = s.child
+ if s is None:
+ break
+ selection.append(s)
+ if backwards:
+ selection.reverse()
+ return selection
+ def tip(self, rev=None):
+ "Return the tip revision of the branch of the given native revision."
+ if rev is None:
+ rev = self.current().native
+ s = self.by_native_d[rev]
+ while True:
+ if s.child is None:
+ return s
+ else:
+ s = s.child
+ def native_to_revno(self, revision):
+ "Map a native ID to a revno"
+ item = self.by_native_d.get(revision)
+ return item and item.revno
+ def by_revno(self, revno):
+ "Map a revno to a revision item."
+ try:
+ return self.by_revno_d[revno]
+ except KeyError:
+ if revno == 0:
+ # This case comes up if we try to select the tip
+ # revision of a history without revisions.
+ croak("{0} has no revisions".format(self.name))
+ else:
+ croak("{0} has no revno {1}".format(self.name, revno))
+ def revno_to_native(self, revno):
+ "Map a revno to a native ID"
+ return self.by_revno(revno).native
+ def set_annotations(self):
+ "Write auxilary symbols as JSON."
+ backend.write_description(json.dumps(self.annotations), self)
+
+help_topics = {
+ "topics": """
+The following help topics are available:
+
+intro -- Basic concepts: commits, tags, branches. The form of commands.
+revisions -- How to specify ranges of commits to operate on.
+commands -- a summary of the commands.
+commit -- the commit command: how to commit changes to a file.
+amend -- the amend command: editing stored change comments.
+checkout -- the checkout command: retrieving historical versions of files.
+cat -- the cat command: dumping revisions to standard output.
+status -- the status command: more details and unusual status codes.
+log -- the log command: dump commit log information to standard output.
+list -- the list command: dump commit summaries to standard output.
+diff -- the diff command: dump revision differences to standard output.
+fast-export -- the fast-export command: export history to other systems.
+fast-import -- the fast-import command: import history from other systems.
+ignores -- .srcignore files and their uses.
+
+The 'help', 'rename', 'ls', 'move', 'copy', 'visualize', and 'version'
+commands are completely described in the command summary.
+""",
+ "intro": """
+SRC Introduction
+
+ SRC (or src) is designed for version control on single-file
+ projects.
+
+ A SRC history is a sequence of commits numbered in strict time order
+ starting from 1. Each holds a modification to the file, a comment,
+ and the date-time of the commit.
+
+ The sequence also has a branch structure. By default there is
+ just one branch named 'trunk'. You can start a new named branch
+ at any commit. Branches can later be renamed or deleted. Because
+ of branching, parent and child commits do not necessarily have
+ consecutive numbers.
+
+ Commits will always be be added to the tip of the current branch.
+ You can change the current branch by either checking out a revision
+ that is on that branch, or using a 'src branch' command to
+ explicitly change the current branch.
+
+ You can assign tags (names) to point to commits. They too can be
+ renamed later or deleted.
+
+ The general form of a SRC command is
+
+ src verb [switches] [revision-spec] [files...]
+
+ That is, a command verb is followed by optional switches, which are
+ (sometimes) optionally followed by a range of commits to operate
+ on, which is optionally followed by a list of files to operate on.
+ Usually you will specify either a revision range or multiple files,
+ but not both.
+
+ The token '--' tells the command-line interpreter that subcommands,
+ switches, and revision-specs are done - everything after it is a
+ filename, even if it looks like a subcommand or revision number.
+
+ If you do not specify any files, SRC will operate sequentially on
+ each individual file in the current directory with a history.
+
+ A good help topics to read after this one would be 'revisions'.
+""",
+ "revisions": """
+SRC Revisions
+
+ A 'revision' is a 1-origin integer, or a tag name designating an
+ integer revision, or a branch name designating the tip revision of
+ its branch, or '@' meaning the currently fetched revision. Revision
+ numbers always increase in commit-date order.
+
+ A revision range is a single revision, or a pair of revisions 'M-N'
+ (all revisions numerically from M to N) or 'M..N' (all revisions
+ that are branch ancestors of N and branch successors of M). If N is
+ less than M, the range is generated as if N >= M then reversed.
+
+ If SRC complains that your revision spec looks like a nonexistent
+ filename, you can prefix it with '@' (this is always allowed).
+
+ Some commands (help, commit, status, delete/rename commands for tags
+ and branches, ls, move, copy, fast-import, release, version) don't
+ take a revision spec at all and will abort if you give one.
+
+ Some commands (amend, checkout, cat, tag and branch creation)
+ optionally take a singleton revision spec.
+
+ Some commands (log, list, diff, fast-export) accept a range or a
+ singleton.
+
+ Unless otherwise noted under individual commands, the default
+ revision is the tip revision on the current branch.
+
+ A good topic to read next would be 'commands'.
+""",
+ "commands": """
+SRC Commands Summary
+
+src help [command]
+ Displays help for commands.
+
+src commit [-|-m 'string'|-f 'file'|-e] ['file'...]
+ Enters a commit for specified files, separately to each one.
+ A history is created for the file if it does not already exist.
+ With '-', take comment text from stdin; with '-m' use the following
+ string as the comment; with '-f' take from a file. With '-e', edit
+ even after '-', '-f' or '-m'. 'ci' is a synonym for 'commit'.
+
+src amend [-|-m 'string'|-f 'file'|-e] ['revision'] ['file'...]
+ Amends the stored comment for a specified revision, defaulting to
+ the latest revision on the current branch. Flags are as for commit.
+
+src checkout ['revision'] ['file'...]
+ Refresh the working copies of the file(s) from their history files.
+ 'co' is a synonym for 'checkout'.
+
+src cat ['revision'] ['file'...]
+ Send the specified revisions of the files to standard output.
+
+src status [-a] ['file'...]
+ '=' = unmodified, 'M' = modified, '!' = missing,
+ '?' = not tracked, 'I' = ignored, 'A' = added,
+ 'L' = locked (recover with 'src checkout').
+ Find more details under 'help status'. 'st' is a synonym for
+ 'status'.
+
+src tag [list|-l|create|-c|delete|del|-d] ['name'] ['revision'] ['file'...]
+ List tags, create tags, or delete tags. Create/delete takes a
+ singleton revision, defaulting to the current branch tip. List
+ defaults to all revisions.
+
+src branch [list|-l|create|-c|delete|del|-d] ['name'] ['file'...]
+ List, create, or delete branches. When listing, the active branch
+ is first in the list. The default branch is 'trunk'. Create/delete
+ takes a singleton revision, defaulting to the current branch tip.
+ List defaults to all revisions, including 0 (the trunk root phantom
+ revision).
+
+src rename ['tag'|'branch'] ['oldname'] ['newname'] ['file'...]
+ Rename a tag or branch. Refuses to step on an existing symbol or
+ rename a nonexistent one. 'rn' is a synonym for 'rename'.
+
+src list [(-<n>|-l <n>)] [-f 'fmt'] ['revision-range'] ['file'...]
+ Sends summary information about the specified commits to standard
+ output. The summary line tagged with '*' is the state that the file
+ would return to on checkout without a revision-spec. See 'help
+ list' for information about custom formats. Use '-<n>' or '-l <n>',
+ where <n> is a number, to limit the listing length. Default range is
+ the current branch, reversed.
+
+src log [-v] [(-<n>|-l <n>)] [(-p|-u|-c) [-b|-w]] ['revision-range'] ['file'...]
+ Sends log information about the specified commits to standard
+ output. Use '-<n>' or '-l <n>', where <n> is a number, to limit the
+ listing length. Default range is the current branch, reversed.
+ Use '--patch', '-p' or '-u' to also send a unified format diff
+ listing to standard output for each revision against its immediate
+ ancestor revision; '-c' emits a context diff instead. When generating
+ a diff, '-b' ignores changes in the amount of whitespace, and '-w'
+ ignores all whitespace. Histories imported via 'fast-import' (when
+ not using its '-p' option) have RFC-822-style headers inserted into
+ the log comment to preserve metadata not otherwise representable in
+ SRC, such as distinct author and committer identifications and
+ dates. These headers are normally suppressed by 'log', however,
+ '-v' shows a summarized view of important headers; '-v -v' shows
+ all headers as-is.
+
+src diff [(-u|-c) [-b|-w]] ['revision-range'] ['file'...]
+ Sends a diff listing to standard output. With no revision spec,
+ diffs the working copy against the last version checked in. With
+ one revno, diffs the working copy against that stored revision; with
+ a range, diff between the beginning and end of the range. 'di' is a
+ synonym for 'diff'.
+
+src ls
+ List all registered files.
+
+src visualize
+ Emit a DOT visualization of repository structures. To use this,
+ install the graphviz package and pipe the output to something like
+ 'dot -Tpng | display -'. 'vis' is a synonym for 'visualize'.
+
+src move 'old' 'new'
+ Rename a workfile and its history. Refuses to step on existing
+ workfiles or histories. 'mv' is a synonym for 'move'.
+
+src copy 'old' 'new'
+ Copy a workfile and its history. Refuses to step on existing files
+ or histories. 'cp' is a synonym for 'copy'.
+
+src fast-export ['revision-range'] ['file'...]
+ Export one or more projects to standard output as a Git fast-import
+ stream. For a history originally imported from elsewhere, author
+ and committer identification is gleaned from the RFC-822-style
+ headers inserted into the commit comment by 'fast-import' (if its
+ '-p' option was not used). Otherwise, this information is copied
+ from your Git configuration. The default range is all commits.
+
+src fast-import [-p] ['file'...]
+ Parse a git-fast-import stream from standard input. The
+ modifications for each individual file become separate SRC
+ histories. Mark, committer and author data, and mark cross-
+ references to parent commits, are preserved in RFC-822-style headers
+ on log comments unless the '-p' (plain) option is given, in which
+ case this metadata is discarded. Give arguments to restrict the
+ files imported.
+
+src release ['file'...]
+ Release locks on files. This is never necessary in a normal
+ workflow, which will be repeated edit-commit cycles, but it may be
+ handy if you have to interoperate with other tools that expect RCS
+ masters to be in their normal (unlocked/unwritable) state.
+
+src version
+ Report the versions of SRC, the underlying Python, and the back end.
+
+The omission of 'src remove' is a deliberate speed bump.
+""",
+ "status": """
+src status [-a] ['file'...]
+
+ The status command shows you the version-control status of files.
+ It is designed to be useful for both humans and software front ends
+ such as Emacs VC mode.
+
+ The status codes, in roughly most common to rarest, are:
+
+ = - Unmodified. File is the same as the latest stored revision.
+ M - Modified. File has been changed since the latest stored revision.
+ ? - Not tracked. SRC does not keep a history for this file.
+ I - ignored. This file matches the patterns in '.srcignore'.
+ ! - Missing. There is a history for this file but the workfile is missing.
+ A - The file has been registered into SRC but has no commits.
+ L - The file is locked/writable.
+
+ Modification status is by content (using a SHA1 hash) rather than
+ the filesystem's last-modified date. Thus, if you make changes to a
+ work file in your editor, then undo them, the file's status returns
+ to '='.
+
+ You can usually recover a file from 'A', 'L', and '!' status with
+ 'src checkout'. 'A' and 'L' statuses should only occur if you have
+ used RCS directly on a file, or if you have called 'src commit' with
+ the deliberately undocumented '-a' option meant for Emacs VC's use.
+
+ If you give 'src status' no filename arguments, it surveys all files
+ in the current directory but untracked and ignored files are not
+ listed. If you give it filename arguments, status is listed for all
+ of them.
+
+ The '-a' option forces status listing of all files. This differs
+ from 'src status *' because the latter will not see dotfiles and
+ thus not list the status of them.
+""",
+ "commit":"""
+src commit [-|-m 'string'|-f 'file'| -e] ['file'...]
+
+ The commit command is how you add revisions to your file history.
+ It always adds the contents of the workfile as a revision to the tip
+ of the current branch.
+
+ You also use commit on files that have not been registered to start
+ an SRC history for them.
+
+ When you commit, you must specify a change comment to go with the
+ revision. There are several ways to do this.
+
+ The '-m' option to the command takes the following string argument
+ as the comment. The '-' option takes the comment text from standard
+ input. The '-f' option takes the comment from a named file.
+
+ If you use none of these, or if you use one of them and the '-e'
+ option, SRC will start an editor in which you can compose the
+ comment. Text specified via '-m', '-f', or '-' becomes the initial
+ contents of the comment.
+
+ SRC respects the EDITOR variable and calls it on a temporary file to
+ create your comment. Th file will have a footer including its name
+ and revision which will be discarded when you finish editing.
+
+ If you leave the comment empty (except for the generated footer)
+ or consisting only of whitespace, the commit will be aborted. The
+ commit will also be aborted if your editor returns a failure status.
+
+ If you commit to multiple files at once, separate changes will be
+ registered for each one, and you may get a separate edit session for
+ each (if you have not set the comment text with options, or have
+ forced editing with '-e'). This is a major difference from other
+ VCSes, which are usually designed to create changesets common to
+ multiple files.
+
+ 'ci' is a synonym for 'commit'.
+""",
+ "amend" : """
+src amend [-|-m 'string'|-f 'file'| -e] ['revision'] ['file'...]
+
+ Use this command to amend (modify) the change comment in a saved
+ revision. The commit date is not changed.
+
+ Takes a singleton revision number, tag, or branch, defaulting to the
+ latest revision on the current branch.
+
+ The edit flags and EDITOR variable are interpreted are as for
+ commit. The only difference is that existing change comment is
+ appended to any text you specify with switches as the initial
+ comment passed to your editor.
+
+ 'am' is a synonym for 'amend'
+""",
+ "checkout":"""
+src checkout ['revision'] ['file'...]
+
+ Refresh the working copies of the file(s) from their history files.
+
+ Takes a single revision number, tag, or branch name. The default if
+ you give none is the tip revision of the current branch.
+
+ This command is how you discard the contents of a modified workfile.
+
+ You can also use it to revert the workfile to match a previous
+ stored revision. Doing do may, as a side effect, change your
+ current branch.
+
+ 'co' is a synonym for 'checkout'.
+""",
+ "cat" : """
+src cat ['revision'] ['file'...]
+
+ Send the specified revision of each file to standard output. This
+ is not normally very useful with more than one file argument, but
+ SRC does not prevent that.
+
+ Takes a single revision number, tag, or branch name. The default if
+ you give none is the tip revision of the current branch.
+
+ This command is mainly intended for use in scripts.
+""",
+ "tag" : """
+src tag [list|-l|create|-c|delete|del|-d] ['name'] ['revision'] ['file'...]
+
+ List tags (with '-l'), create tags (with '-c'), or delete tags (with
+ '-d').
+
+ Takes at most a singleton revision; the default is the current
+ branch tip.
+
+ Tag creation and deletion require a following name argument. Tag
+ creation will not step on an existing tag name, and a nonexistent
+ branch cannot be deleted.
+""",
+ "branch" : """
+src branch [list|-l|create|-c|delete|del|-d] ['name'] ['file'...]
+
+ List branches (with '-l'), create branches (with '-c'), or delete
+ branches (with '-d').
+
+ In the list produced by '-', the active branch is first in the list.
+
+ Branch creation and deletion require a following name argument.
+ Branch creation will not step on an existing branch name, and a
+ nonexistent branch cannot be deleted.
+""",
+ "log" : """
+src log [-v] [(-<n>|-l <n>)] [(-p|-u|-c) [-b|-w]] ['revision-range'] ['file'...]
+
+ Sends log information about the specified commits of each file to
+ standard output. The log information includes the revision number,
+ the date, and the log comment.
+
+ With no revision, dumps a log of the entire current branch.
+
+ The '--patch', '-p' or '-u' option additionally sends a unified
+ format diff listing to standard output for each revision against its
+ immediate ancestor revision; '-c' emits a context diff instead. When
+ generating a diff, '-b' ignores changes in the amount of whitespace,
+ and '-w' ignores all whitespace.
+
+ The '-<n>' or '-l <n>' option, where <n> is a number, can be used to
+ limit the listing length.
+
+ Histories imported via 'fast-import' (when not using its '-p' option)
+ have RFC-822-style headers inserted into the log comment to preserve
+ metadata not otherwise representable in SRC, such as distinct author
+ and committer identifications and dates. These headers are normally
+ suppressed by 'log', however, '-v' shows a summarized view of
+ important headers; '-v -v' shows all headers as-is.
+""",
+ "list" : """
+src list [(-<n>|-l <n>)] [-f 'fmt'] ['revision-range'] ['file'...]
+
+ Sends summary information about the specified commits of each file
+ to standard output. The summary information includes the revision
+ number, the date, and the first line of the log comment.
+
+ This command is provided assuming you will use the good practice of
+ beginning each commit with a self-contained summary line.
+
+ With no revision, dumps a log of the entire current branch.
+
+ The '-f' option allows you to set a custom format string. Available
+ substitutions are:
+
+ {0} - the file name
+ {1} - the revision number
+ {2} - the mark '*' if this is the currently checked out revision, else '-'.
+ {3} - the date in RFC3339 format
+ {4} - the summary line
+
+ The '-<n>' or '-l <n>' option, where <n> is a number, can be used to
+ limit the listing length.
+
+ 'li' is a synonym for 'list'.
+""",
+ "diff" : """
+src diff [(-u|-c) [-b|-w]] ['revision-range'] ['file'...]
+
+ Sends a diff listing to standard output.
+
+ With no revision spec, diffs the working copy against the last
+ version checked in. With one revno, diffs the working copy against
+ that stored revision; with a range, diff between the beginning and
+ end of the range.
+
+ The actual difference generation is done with diff(1). The default
+ diff format is '-u' (unified), but if you specify a '-c' option
+ after the verb a context diff will be emitted. '-b' ignores changes
+ in the amount of whitespace, and '-w' ignores all whitespace.
+
+ 'di' is a synonym for 'diff'.
+""",
+ "fast-export" : """
+src fast-export ['revision-range'] ['file'...]
+
+ Export one or more projects to standard output as a git fast-import
+ stream. This can be consumed by 'git fast-import' to create a Git
+ repository containing the project history.
+
+ It is possible (though probably not very useful) to fast-export a
+ limited range of commits, producing an incremental dump. In this
+ case branch joins are done with the magic ^0 suffix.
+
+ Fast-exporting multiple files produces a single stream with a joint
+ history.
+
+ For a history originally imported from elsewhere, author and
+ committer identification is gleaned from the RFC-822-style headers
+ inserted into the commit comment by 'fast-import' (if its '-p'
+ option was not used). Otherwise, this information is copied from
+ your Git configuration.
+
+ The default range is all commits.
+""",
+ "fast-import" : """
+src fast-import [-p] ['file'...]
+
+ Parse a git-fast-import stream from standard input. The
+ modifications for each individual file become separate SRC
+ histories. Give arguments to restrict the files imported.
+
+ The import is actually done with the rcs-fast-import(1) tool, which
+ must be on your $PATH for this command to work.
+
+ Some gitspace metadata cannot be represented in the SRC/RCS
+ model of version control. Mark, committer and author data, and
+ mark cross-references to parent commits. These are preserved in
+ RFC-822-style headers on log comments unless the '-p' (plain) option
+ is given, in which case this metadata is discarded.
+""",
+ "ignores" : """
+Making SRC Ignore Certain Files
+
+ You can have a file named '.srcignore' containing the names of files
+ that SRC should ignore, or more commonly patterns describing files
+ to ignore.
+
+ When SRC is told to ignore a file, it won't show up in 'src status'
+ listings unless the '-a' (all) flag is used or you give it as an
+ explicit argument. It will also be ignored when commands that
+ expect a list of registered files see it (which could easily happen
+ when you use shell wildcards in SRC commands).
+
+ Other version-control systems have these too. The classic example
+ of how to do this is using the pattern '*.o' to ignore C object
+ files. But if you need to do that, you should probably be using a
+ multi-file VCS with changesets, not this one.
+
+ Patterns that might be useful with single-file projects include
+ '*~' to ignore editor backup files, or '*.html' if you're writing
+ documents that render to HTML but aren't sourced in it.
+
+ The repo subdirectory - normally '.src' - is always ignored, but
+ '.srcignore' itself is not automatically ignored.
+
+ SRC's pattern syntax is that of Unix glob(3), with initial '!'
+ treated as a negation operator. This is forward-compatible to Git's
+ ignore syntax.
+
+ * matches any string of characters.
+ ? matches any single character.
+ [] brackets a character class; it matches any character in the
+ class. So, for example, [0123456789] would match any decimal
+ digit.
+ [!] brackets a negated character class; [!0123456789] would match
+ any character not a decimal digit.
+""",
+ }
+
+def help_method(*args):
+ "Summarize src commands, or (with argument) show help for a single command."
+ if not args:
+ sys.stdout.write(help_topics['topics'])
+ for arg in args:
+ if arg in args:
+ if arg in help_topics:
+ sys.stdout.write(help_topics[arg])
+ else:
+ croak("%s is not a known help topic.\n%s" % (arg, help_topics['topics']))
+
+def parse_as_revspec(token):
+ "Does this look like something that should be parsed as a revision spec?"
+ if "/" in token:
+ return False
+ elif ".." in token:
+ return True
+ elif token.count("-") == 1 and '.' not in token:
+ return True
+ elif token.isdigit():
+ return True
+ elif token.startswith("@"): # Escape clause for tags that look like files
+ return True
+ else:
+ return False
+
+ignorable = None
+
+def ignore(filename):
+ "Should the specified file be ignored?"
+ global ignorable
+ if ignorable is None:
+ ignorable = set([])
+ ignorable = set()
+ if os.path.exists(".srcignore"):
+ with open(".srcignore", "rb") as fp:
+ for line in fp:
+ # Use polystr to ensure internal data is Unicode in Python 3
+ line = polystr(line)
+ if line.startswith("#") or not line.strip():
+ continue
+ elif line.startswith("!"):
+ ignorable -= set(glob.glob(line[1:].strip()))
+ else:
+ ignorable |= set(glob.glob(line.strip()))
+ return (filename == repodir) or (filename in ignorable)
+
+class CommandContext:
+ "Consume a revision specification or range from an argument list"
+ def __init__(self, cmd, args,
+ require_empty=False,
+ default_to=None,
+ parse_revspec=True):
+ if not os.path.exists(repodir):
+ croak("repository subdirectory %s does not exist" % repodir)
+ self.start = self.end = None
+ self.seq = None
+ self.branchwise = None
+ if type(args) == type(()):
+ args = list(args)
+ self.args = list(args)
+ self.default_to = default_to
+ self.lo = self.start
+ self.hi = self.end
+ revspec = None
+ if self.args:
+ if self.args[0] == "--":
+ self.args.pop(0)
+ elif parse_revspec and parse_as_revspec(args[0]):
+ revspec = self.args.pop(0)
+ if revspec.startswith("@"):
+ revspec = revspec[1:]
+ try:
+ if "-" in revspec or ".." in revspec:
+ self.branchwise = ".." in revspec
+ try:
+ (self.start, self.end) = revspec.split("-")
+ except ValueError:
+ try:
+ (self.start, self.end) = revspec.split("..")
+ except ValueError:
+ croak("internal error - argument parser is confused")
+ try:
+ self.start = int(self.start)
+ except ValueError:
+ pass
+ try:
+ self.end = int(self.end)
+ except ValueError:
+ pass
+ else:
+ try:
+ self.end = self.start = int(revspec)
+ except ValueError:
+ self.end = self.start = revspec
+ except ValueError:
+ croak("malformed revision spec: %s" % revspec)
+ if require_empty and not self.is_empty():
+ croak("%s doesn't take a revision spec" % cmd)
+ if not self.args:
+ try:
+ masters = [fn for fn in os.listdir(repodir) if is_history(fn)]
+ masters.sort()
+ except OSError:
+ croak("repo directory %s does not exist" % repodir)
+ if masters:
+ self.args += [backend.workfile(master) for master in masters]
+ else:
+ croak("%s requires at least one file argument" % cmd)
+ def is_empty(self):
+ "Is the spec empty?"
+ return self.start is None
+ def is_singleton(self):
+ "Is the spec a singleton?"
+ return self.start is not None and self.start == self.end
+ def is_range(self):
+ "Is the spec a range?"
+ return self.start is not None and self.start != self.end
+ def select_all(self, metadata):
+ "Set the range to all revisions."
+ self.lo = 1
+ self.hi = len(metadata)
+ self.seq = [metadata.by_revno(i) for i in range(self.lo, self.hi+1)]
+ def select_tip(self, metadata):
+ "Set the range to the tip revision."
+ self.lo = len(metadata)
+ self.hi = None
+ self.seq = [metadata.by_revno(self.lo)]
+ def __contains__(self, i):
+ "Does the spec contain the given revno?"
+ if self.seq is None:
+ croak("revision spec hasn't been resolved")
+ return i in self.seq
+ def resolve(self, metadata):
+ "Resolve a revision spec that may contain tags into revnos."
+ if debug >= DEBUG_SEQUENCE:
+ sys.stderr.write("Entering resolve with start=%s, end=%s\n" % (self.start, self.end))
+ if self.is_empty():
+ if self.default_to == "branch":
+ self.seq = metadata.current_branch(backwards=False)
+ elif self.default_to == "branch_reversed":
+ self.seq = metadata.current_branch(backwards=True)
+ else:
+ self.seq = []
+ return self.seq
+ def subresolve(token):
+ part = token
+ if type(part) == type(0):
+ return part
+ if token == '': # User specified @
+ current = metadata.current()
+ if current is None:
+ croak("in {0}, no current revision".format(metadata.name))
+ return current.revno
+ if part not in metadata.symbols:
+ croak("in {0}, can't resolve symbol {1}".format(metadata.name,token))
+ else:
+ part = metadata.symbols[part]
+ if backend.isbranch(part):
+ part = backend.branch_to_tip(part, metadata)
+ return metadata.native_to_revno(part)
+ self.lo = subresolve(self.start)
+ self.hi = subresolve(self.end)
+ mreversed = (self.lo > self.hi)
+ if mreversed:
+ swapme = self.hi
+ self.hi = self.lo
+ self.lo = swapme
+ if self.hi > len(metadata):
+ croak("{0} has no {1} revision".format(metadata.name, self.hi))
+ if not self.branchwise:
+ self.seq = [metadata.by_revno(i) for i in range(self.lo, self.hi+1)]
+ else:
+ self.seq = []
+ e = metadata.by_revno(self.hi)
+ while True:
+ self.seq.append(e)
+ if e.revno == self.lo:
+ break
+ if e.parent is None:
+ croak("%s is not an ancestor of %s" % (self.lo, self.hi))
+ else:
+ e = e.parent
+ if debug >= DEBUG_SEQUENCE:
+ sys.stderr.write("selection: %s, branchwise is %s\n" % ([x.revno for x in self.seq], "on" if self.branchwise else "off"))
+ for item in metadata.revlist:
+ sys.stdout.write("%s\t%s\t%s\n" % (item.revno, item.date, item.native))
+ # Because in the branchwise case the sequence is generated in reverse
+ if self.branchwise:
+ self.seq.reverse()
+ # Range might have been reversed
+ if mreversed:
+ self.seq.reverse()
+ return self.seq
+
+class CommentContext:
+ COMMENT_CUTLINE = """\
+.............................................................................
+"""
+ COMMENT_EXPLANATION = """\
+The cut line and things below it will not become part of the comment text.
+"""
+ def __init__(self, legend, args):
+ "Attempt to collect a comment from command line args."
+ self.leader = ""
+ self.comment = None
+ self.force_edit = False
+ self.parse_revspec = True
+ if args:
+ if args[0] == '--':
+ self.parse_revspec = False
+ args.pop(0)
+ elif args[0] == "-":
+ self.leader = sys.stdin.read()
+ args.pop(0)
+ elif args[0] == "-e":
+ self.force_edit = True
+ elif args[0] == "-m":
+ args.pop(0)
+ try:
+ self.leader = args[0]
+ args.pop(0)
+ except IndexError:
+ croak("%s -m requires a following string" % legend)
+ elif args[0].startswith("-m"):
+ self.leader = args[2:]
+ args.pop(0)
+ elif args[0] == "-f":
+ args.pop(0)
+ try:
+ # Read filesystem data as binary for robustness, but
+ # decode to Unicode for internal use
+ with open(args[0], "rb") as fp:
+ self.leader = polystr(fp.read())
+ args.pop(0)
+ except IndexError:
+ croak("%s -f requires a following filename argument" % legend)
+ except OSError:
+ croak("couldn't open %s." % args[1])
+ elif args[0].startswith("-"):
+ croak("unexpected %s option" % args[0])
+ def edit(self, content="", trailer="", diff=None):
+ "Interactively edit a comment if required, then prepare for handoff."
+ if self.leader and not self.force_edit:
+ self.comment = self.leader
+ else:
+ orig_content = content
+ if self.leader:
+ content = self.leader + content
+ if trailer or diff:
+ content += "\n" + CommentContext.COMMENT_CUTLINE + CommentContext.COMMENT_EXPLANATION + trailer
+ editor = os.getenv("EDITOR") or "emacsclient"
+ try:
+ # Use polybytes to ensure that binary data is written
+ # on Python 3
+ with tempfile.NamedTemporaryFile(prefix = "src", suffix = "tmp", delete = False) as fp:
+ commentfile = fp.name
+ fp.write(polybytes(content))
+ if diff:
+ fp.write(b"\nChanges to be committed:\n")
+ for s in diff:
+ fp.write(polybytes(s))
+ fp.write(b"\n")
+ do_or_die(editor + " " + commentfile, mute=False)
+ # Use polystr to ensure that incoming data is decoded
+ # to Unicode on Python 3
+ with open(commentfile, "rb") as fp:
+ self.comment = polystr(fp.read())
+ os.unlink(commentfile)
+ except IOError:
+ croak("edit aborted.")
+ where = self.comment.find(CommentContext.COMMENT_CUTLINE)
+ if where != -1:
+ self.comment = self.comment[:where]
+ self.comment = self.comment.strip()
+ if self.comment == orig_content.strip() or not self.comment:
+ return False
+ # Can be removed if we ever parse RCS/SCCS files directly
+ for badnews in backend.delimiters:
+ if badnews in self.comment:
+ croak("malformed comment")
+ if not self.comment.endswith("\n"):
+ self.comment += "\n"
+ return True
+ def content(self):
+ "Return the edited comment."
+ return self.comment
+
+def external_diff(file0, s0, file1, s1, unified=True, ignore_ws=None):
+ "Compute diff using external program."
+ def writetmp(s):
+ with tempfile.NamedTemporaryFile(prefix = "src", suffix = "tmp", delete = False) as fp:
+ fp.write(polybytes(s))
+ return fp.name
+ if unified:
+ opts, tok0, tok1 = '-u', '-', '+'
+ else:
+ opts, tok0, tok1 = '-c', '*', '-'
+ if ignore_ws:
+ opts += ' ' + ignore_ws
+ tmp0 = writetmp(s0)
+ tmp1 = writetmp(s1)
+ with popen_or_die('diff %s "%s" "%s" || :' % (opts, tmp0, tmp1)) as fp:
+ diff = polystr(fp.read())
+ os.unlink(tmp0)
+ os.unlink(tmp1)
+ diff = re.sub(r"(?m)^([%s]{3} ).+$" % tok0, r"\1" + file0, diff, 1)
+ diff = re.sub(r"(?m)^([%s]{3} ).+$" % tok1, r"\1" + file1, diff, 1)
+ return diff.split('\n')
+
+def compute_diff(metadata, lo, hi, differ, ignore_ws=None):
+ """Compute diff between two revs of a file.
+ If 'lo' is None, then this is a "creation event" in which 'hi'
+ materializes fully formed from nothing.
+ If 'hi' is None, then diff 'lo' against the working file.
+ File must already have been "lifted"."""
+ name = metadata.name
+ if lo is None:
+ file0 = '/dev/null'
+ old_content = ''
+ else:
+ file0 = name + ' (r' + str(lo) + ')'
+ with backend.cat(name, metadata.revno_to_native(lo)) as fp:
+ old_content = fp.read() # this data will be binary
+ if hi is None:
+ file1 = name + ' (workfile)'
+ with open(name, "rb") as fp:
+ new_content = fp.read()
+ else:
+ file1 = name + ' (r' + str(hi) + ')'
+ with backend.cat(name, metadata.revno_to_native(hi)) as fp:
+ new_content = fp.read() # this data will be binary
+ # Don't list identical files (comparison uses binary data
+ # for maximum accuracy).
+ if old_content == new_content:
+ return ()
+ # All operations here will need Unicode in Python 3
+ if ignore_ws:
+ return external_diff(file0, old_content, file1, new_content,
+ differ == difflib.unified_diff, ignore_ws)
+ lines0 = polystr(old_content).split('\n')
+ lines1 = polystr(new_content).split('\n')
+ return differ(lines0, lines1,
+ fromfile=file0,
+ tofile=file1,
+ lineterm="")
+
+def colorize_unified(s):
+ for x in colorize_unified.colors:
+ if s.startswith(x[0]):
+ return x[1] + s + RESET
+ return s
+
+colorize_unified.colors = (('+++ ', BOLD), ('--- ', BOLD), ('@@ ', CCYAN),
+ ('+', CGREEN), ('-', CRED))
+
+def print_diff(metadata, lo, hi, differ, ignore_ws=None):
+ "Dump diff between revisions to standard output."
+ if differ == difflib.unified_diff:
+ colorizer = colorize_unified
+ else:
+ colorizer = lambda x: x
+ with backend.lifter(metadata.name):
+ for line in compute_diff(metadata, lo, hi, differ, ignore_ws):
+ sys.stdout.write(colorizer(line) + "\n")
+
+def commit_method(*args):
+ "Commit changes to files."
+ if not os.path.exists(repodir):
+ try:
+ os.mkdir(repodir)
+ except OSError:
+ croak(" %s creation failed, check directory permissions." % repodir)
+ args = list(args)
+ register_only = False
+ parse_revspec = True
+ differ = difflib.unified_diff
+ while args:
+ if args[0] == '--':
+ args.pop(0)
+ parse_revspec = False
+ break
+ elif args[0] == '-a':
+ # The Emacs VC-mode support for SRC was written early,
+ # before 1.0, when I hadn't quite figured out what the
+ # most efficient method for file reigstration would be.
+ # VC-mode wants to have a registration as well as a
+ # checkin method. This is no longer needed for normal SRC
+ # operation, but it's better to leave this in place than
+ # risk causing hassles for people running old Enacs versions.
+ register_only = True
+ parse_revspec = False
+ args.pop(0)
+ else:
+ break
+ if not register_only:
+ comment = CommentContext("commit", args)
+ ctx = CommandContext("commit", args,
+ require_empty=True,
+ parse_revspec=parse_revspec and comment.parse_revspec)
+ for arg in ctx.args:
+ if not os.path.exists(arg):
+ croak("I see no '%s' here." % arg)
+ if os.path.isdir(arg):
+ croak("cannot commit directory '%s'" % arg)
+ for arg in ctx.args:
+ if not registered(arg):
+ trailer = "Committing initial revision of {0}.\n".format(arg)
+ revcount = 0
+ metadata = None
+ diff = compute_diff(type('', (), {'name':arg}), None, None, differ)
+ elif register_only:
+ croak("attempt to re-add a registered file failed")
+ else:
+ metadata = History(arg)
+ ctx.resolve(metadata)
+ if metadata and ctx.is_empty():
+ ctx.select_tip(metadata)
+ revcount = len(metadata)
+ trailer = "Committing {0} revision {1}.\n".format(arg, revcount+1)
+ with backend.lifter(arg):
+ diff = compute_diff(metadata, ctx.lo, None, differ)
+ if not register_only and not diff:
+ announce("in %s, no changes to commit" % arg)
+ continue
+ if not register_only and not comment.edit('', trailer, diff):
+ announce("in %s, commit cancelled" % arg)
+ else:
+ if not registered(arg):
+ backend.register(arg)
+ if not register_only:
+ with backend.lifter(arg):
+ # If the user changed the executable bit while
+ # modifying the workfile, propagate this change to
+ # the master. Without this hack, the sequence (1)
+ # Commit workfile (2) Make workfile executable, (3)
+ # checkin workfile fails to work as expected because
+ # the VCS doesn't propagate the changed executable
+ # bit to the master, leading to misbehavior on the
+ # next checkout.
+ master = os.path.basename(backend.history(arg))
+ if os.path.exists(master):
+ oldmastermode = newmastermode = os.stat(master).st_mode
+ userworkmode = os.stat(arg).st_mode
+ for bitmask in (stat.S_IXUSR, stat.S_IXGRP, stat.S_IXOTH):
+ if bitmask & userworkmode:
+ newmastermode |= bitmask
+ else:
+ newmastermode &=~ bitmask
+ if newmastermode != oldmastermode:
+ os.chmod(master, newmastermode)
+ backend.checkin(arg, comment.content())
+ if metadata is None:
+ metadata = History(arg)
+ modified(arg, metadata)
+ if not quiet and len(args) > 1:
+ announce("%s -> %d" % (arg, revcount))
+
+def add_method(*args):
+ "For Emacs VC compatibility."
+ commit_method("-a", *args)
+
+def amend_method(*args):
+ "Amend comments in stored revisions."
+ if not os.path.exists(repodir):
+ croak("repository subdirectory %s does not exist" % repodir)
+ args = list(args)
+ differ = difflib.unified_diff
+ comment = CommentContext("amend", args)
+ ctx = CommandContext("amend", args, parse_revspec=comment.parse_revspec)
+ if ctx.is_range():
+ croak("amend cannot take a range")
+ for arg in ctx.args:
+ if not os.path.exists(arg):
+ croak("I see no '%s' here." % arg)
+ elif not registered(arg):
+ croak("%s is not registered." % arg)
+ for arg in ctx.args:
+ metadata = History(arg)
+ ctx.resolve(metadata)
+ if ctx.is_empty():
+ ctx.lo = metadata.tip().revno
+ trailer ="Amending {0} revision {1}.\n".format(arg, ctx.lo)
+ item = metadata.by_revno(ctx.lo)
+ parent = item.parent.revno if item.parent else None
+ with backend.lifter(arg):
+ diff = compute_diff(metadata, parent, ctx.lo, differ)
+ if not comment.edit(metadata.by_revno(ctx.lo).log, trailer, diff):
+ announce("in %s, amend cancelled" % arg)
+ else:
+ with backend.lifter(arg):
+ backend.amend(arg,
+ metadata.revno_to_native(ctx.lo),
+ comment.content())
+ if not quiet and len(args) > 1:
+ announce("%s : %d" % (arg, ctx.lo))
+
+def list_method(*args):
+ "Generate a summary listing of commits, one line per commit."
+ args = list(args)
+ custom = None
+ limit = None
+ parse_revspec = True
+ while args and args[0].startswith("-"):
+ if args[0] == '--':
+ parse_revspec = False
+ args.pop(0)
+ break
+ elif args[0] == "-f":
+ args.pop(0)
+ try:
+ custom = args[0]
+ args.pop(0)
+ except IndexError:
+ croak("list -f requires a following string")
+ elif args[0].startswith("-f"):
+ custom = args[0][2:]
+ args.pop(0)
+ elif args[0] == "-l":
+ args.pop(0)
+ try:
+ limit = args[0]
+ args.pop(0)
+ limit = int(limit)
+ except IndexError:
+ croak("list -l requires a following integer")
+ except ValueError:
+ croak("%s is not an integer" % limit)
+ elif args[0][1:].isdigit():
+ limit = int(args.pop(0)[1:]) # it's all digits, so no ValueError
+ else:
+ croak("unexpected %s option" % args[0])
+ ctx = CommandContext("list", args,
+ default_to="branch_reversed",
+ parse_revspec=parse_revspec)
+ for arg in ctx.args:
+ if ignore(arg) or os.path.isdir(arg) or not registered(arg):
+ continue
+ if custom is None:
+ sys.stdout.write("= %s %s\n" % (arg, ((WIDTH - len(arg) - 3) * "=")))
+ for item in ctx.resolve(History(arg)):
+ # Must allow enough room for revno and date
+ if item.selected():
+ mark = "*"
+ else:
+ mark = "-"
+ summary = item.log.split('\n')[0]
+ if custom is None:
+ summary = summary[:WIDTH - 34]
+ sys.stdout.write("%-4d %s %s %s\n" \
+ % (item.revno, mark, item.getdate('author'), summary))
+ else:
+ sys.stdout.write(custom.format(arg, item.revno, mark, item.getdate('author'), summary))
+ if limit is not None:
+ limit -= 1
+ if limit <= 0:
+ break
+
+def log_method(*args):
+ "Report revision logs"
+ limit = None
+ args = list(args)
+ parse_revspec = True
+ differ = None
+ ignore_ws = None
+ verbose = 0
+ while args and args[0].startswith("-"):
+ if args[0] == '--':
+ parse_revspec = False
+ args.pop(0)
+ break
+ elif args[0] == "-l":
+ args.pop(0)
+ try:
+ limit = args[0]
+ args.pop(0)
+ limit = int(limit)
+ except IndexError:
+ croak("list -l requires a following integer")
+ except ValueError:
+ croak("%s is not an integer" % limit)
+ elif args[0][1:].isdigit():
+ limit = int(args.pop(0)[1:]) # it's all digits, so no ValueError
+ elif args[0] in ('--patch', '-p', '-u'):
+ differ = difflib.unified_diff
+ args.pop(0)
+ elif args[0] == '-c':
+ differ = difflib.context_diff
+ args.pop(0)
+ elif args[0] in ('-b', '-w'):
+ ignore_ws = args.pop(0)
+ elif args[0] == '-v':
+ verbose += 1
+ args.pop(0)
+ else:
+ croak("unexpected %s option" % args[0])
+ ctx = CommandContext("log", args,
+ default_to="branch_reversed",
+ parse_revspec=parse_revspec)
+ for arg in ctx.args:
+ if ignore(arg) or os.path.isdir(arg) or not registered(arg):
+ continue
+ sys.stdout.write("= %s %s\n" % (arg, ((WIDTH - len(arg) - 3) * "=")))
+ metadata = History(arg)
+ for item in ctx.resolve(metadata):
+ sys.stdout.write("%s%-4d%s | %s%s%s | %s%s%s\n" \
+ % (BOLD + CCYAN, item.revno, RESET,
+ CYELLOW, item.getdate('author'), RESET,
+ BOLD + CGREEN, item.branch, RESET))
+ if verbose and item.headers:
+ headers = item.headers
+ if verbose == 1:
+ headers = {}
+ for k in ('author', 'committer'):
+ if k in item.headers:
+ v = item.headers[k]
+ if k + "-date" in item.headers:
+ v += " " + item.headers[k + "-date"]
+ headers[k] = v
+ for k in sorted(headers.keys()):
+ sys.stdout.write("%s%s%s: %s\n" %
+ (BOLD, k.title(), RESET, headers[k]))
+ sys.stdout.write("\n")
+ sys.stdout.write("%s" % item.log)
+ if differ:
+ if item.parent:
+ parent = item.parent.revno
+ pdesc = 'r%d/%s' % (parent, arg)
+ else:
+ parent = None
+ pdesc = '/dev/null'
+ sys.stdout.write("\n%sdiff %s r%s/%s%s\n" %
+ (BOLD, pdesc, item.revno, arg, RESET))
+ print_diff(metadata, parent, item.revno, differ, ignore_ws)
+ sys.stdout.write(("-" * WIDTH) + "\n")
+ if limit is not None:
+ limit -= 1
+ if limit <= 0:
+ break
+
+def checkout_method(*args):
+ "Refresh the working copy from the history file."
+ ctx = CommandContext("checkout", args)
+ if ctx.is_range():
+ croak("checkout needs an empty or singleton revision spec")
+ for arg in ctx.args:
+ metadata = History(arg)
+ ctx.resolve(metadata)
+ if ctx.is_empty():
+ ctx.select_tip(metadata)
+ revision = ""
+ elif ctx.lo > len(metadata):
+ croak("%s has only %d revisions" % (arg, len(metadata)))
+ else:
+ revision = metadata.revno_to_native(ctx.lo)
+ with backend.lifter(arg):
+ backend.checkout(arg, revision)
+ if not quiet and len(args) > 1:
+ announce("%s <- %d" % (arg, ctx.lo))
+
+def status_method(*args):
+ "Get status of some or all files."
+ try:
+ managed = [fn for fn in os.listdir(repodir) if is_history(fn)]
+ except OSError:
+ croak("repo directory %s does not exist" % repodir)
+ args = list(args)
+ allflag = False
+ if args:
+ if args[0] == '--':
+ args.pop(0)
+ elif args[0] == '-a':
+ allflag = True
+ args.pop(0)
+ if args:
+ candidates = args
+ else:
+ candidates = [f for f in os.listdir(".") if f != repodir]
+ pairs = []
+ for fn in candidates:
+ if ignore(fn):
+ if allflag or fn in args:
+ pairs.append((fn, "I"))
+ continue
+ masterbase = os.path.basename(backend.history(fn))
+ if masterbase not in managed:
+ if allflag or fn in args:
+ if not os.access(fn, os.R_OK):
+ croak("%s does not exist or is unreadable." % fn)
+ else:
+ pairs.append((fn, "?"))
+ elif not os.path.exists(fn):
+ pairs.append((fn, "!"))
+ elif modified(fn):
+ pairs.append((fn, "M"))
+ elif not os.access(fn, os.W_OK):
+ pairs.append((fn, "L"))
+ elif not backend.has_revisions(fn):
+ pairs.append((fn, "A"))
+ else:
+ pairs.append((fn, "="))
+ if not args:
+ for m in managed:
+ if backend.workfile(m) not in candidates:
+ pairs.append((m, "!"))
+ pairs.sort()
+ for (fn, status) in pairs:
+ sys.stdout.write(status + "\t" + fn + "\n")
+
+def cat_method(*args):
+ "Dump revision content to standard output."
+ ctx = CommandContext("cat", args)
+ if ctx.is_range():
+ croak("cat refuses to cough up a hairball")
+ elif ctx.args[0] == '--':
+ ctx.args.pop(0)
+ for arg in ctx.args:
+ metadata = History(arg)
+ ctx.resolve(metadata)
+ if ctx.is_empty():
+ ctx.select_tip(metadata)
+ with backend.lifter(arg):
+ for item in ctx.seq:
+ with backend.cat(arg, item.native) as fp:
+ # Use polystr to ensure that sys.stdout gets Unicode
+ # in Python 3.
+ sys.stdout.write(polystr(fp.read()))
+
+def diff_method(*args):
+ "Dump diffs between revisions to standard output."
+ if type(args) == type(()):
+ args = list(args)
+ differ = difflib.unified_diff
+ ignore_ws = None
+ while args and args[0] != '--' and args[0].startswith("-"):
+ if args[0] == '-u':
+ differ = difflib.unified_diff
+ args.pop(0)
+ elif args[0] == '-c':
+ differ = difflib.context_diff
+ args.pop(0)
+ elif args[0] in ('-b', '-w'):
+ ignore_ws = args.pop(0)
+ else:
+ croak("unexpected %s option" % args[0])
+ ctx = CommandContext("diff", args)
+ for arg in ctx.args:
+ metadata = History(arg)
+ ctx.resolve(metadata)
+ if ctx.is_empty():
+ ctx.select_tip(metadata)
+ print_diff(metadata, ctx.lo, ctx.hi, differ, ignore_ws)
+
+def tag_helper(args, legend, validation_hook, delete_method, set_method):
+ "Dispatch to handlers for tag and branch manipulation."
+ if not os.path.exists(repodir):
+ croak("repository subdirectory %s does not exist" % repodir)
+ args = list(args)
+ if not args:
+ args = ["list"] + args
+ if args[0] == '--':
+ args.pop(0)
+ else:
+ if args[0] in ("-d", "del", "delete"):
+ args.pop(0)
+ if not args:
+ croak("%s deletion requires a name argument" % legend)
+ name = args.pop(0)
+ ctx = CommandContext(legend, args)
+ if not ctx.is_empty():
+ croak("can't accept a revision-spec when deleting a %s." % legend)
+ for arg in ctx.args:
+ metadata = History(arg)
+ if name not in metadata.symbols:
+ croak("in %s, %s is not a symbol" % (arg, name))
+ elif backend.isbranch(metadata.symbols[name]) != (legend == "branch"):
+ croak("in %s, %s is not a %s" % (arg, name, legend))
+ else:
+ with backend.lifter(arg):
+ delete_method(name, metadata)
+ if not quiet and len(args) > 1:
+ announce("in %s, %s %s removed" % (arg, legend, name))
+ return
+ if args[0] in ("-l", "list"):
+ args.pop(0)
+ ctx = CommandContext(legend + " listing", args, require_empty=True)
+ for arg in ctx.args:
+ metadata = History(arg)
+ ctx.resolve(metadata)
+ if ctx.is_empty():
+ prepend = [0]
+ ctx.select_all(metadata)
+ else:
+ prepend = []
+ revisions = prepend + [item.revno for item in ctx.seq]
+ sys.stdout.write("= %s %s\n" \
+ % (arg, ((WIDTH - len(arg) - 5) * "=")))
+ keys = list(metadata.symbols.keys())
+ if metadata.branch in keys:
+ keys.remove(metadata.branch)
+ keys.sort()
+ keys = [metadata.branch] + keys
+ for key in keys:
+ value = metadata.symbols[key]
+ if legend == "branch":
+ # Note! This code relies on
+ # backend.branch_to_parent() returning an empty
+ # string when called on a trunk revision.
+ displaystr = backend.branch_to_parent(value)
+ if not displaystr:
+ display = 0
+ else:
+ display = metadata.native_to_revno(displaystr)
+ else:
+ display = metadata.native_to_revno(value)
+ if display in revisions and backend.isbranch(value) == (legend == "branch"):
+ sys.stdout.write("%4s\t%s\n" % (display,key))
+ return
+ if args[0] in ("-c", "create"):
+ args.pop(0)
+ if not args:
+ croak("%s setting requires a name argument" % legend)
+ name = args.pop(0)
+ ctx = CommandContext(legend, args)
+ if ctx.is_range():
+ croak("can't accept a range when setting a %s" % legend)
+ for arg in ctx.args:
+ metadata = History(arg)
+ revision = validation_hook(ctx, metadata, name)
+ with backend.lifter(arg):
+ set_method(name, revision, metadata)
+ if not quiet and len(args) > 1:
+ announce("in %s, %s %s = %s" % (arg, legend, name, ctx.start))
+ return
+ else:
+ croak("%s requires a list, create, or delete modifier" % legend)
+
+def tag_method(*args):
+ "Inspect, create, and delete tags."
+ def tag_set_validate(ctx, metadata, name):
+ ctx.resolve(metadata)
+ if name in metadata.symbols:
+ croak("tag %s already set." % name)
+ if ctx.is_empty():
+ ctx.select_tip(metadata)
+ return metadata.revno_to_native(ctx.lo)
+ tag_helper(args, "tag",
+ tag_set_validate,
+ backend.delete_tag, backend.set_tag)
+
+def branch_method(*args):
+ "Inspect, create, and delete branches."
+ def branch_set_validate(ctx, _metadata, _name):
+ if not ctx.is_empty():
+ croak("cannot accept a revision after a branch name")
+ tag_helper(args, "branch",
+ branch_set_validate,
+ backend.delete_branch, backend.set_branch)
+
+def rename_method(*args):
+ "Rename a branch or tag."
+ args = list(args)
+ if not args or args[0] not in ("tag", "branch"):
+ croak("rename requires a following 'tag' or 'branch'")
+ legend = args.pop(0)
+ if not args:
+ croak("rename requires a source name argument")
+ name = args.pop(0)
+ if not args:
+ croak("rename requires a target name argument")
+ newname = args.pop(0)
+ ctx = CommandContext(legend + " renaming", args, require_empty=True)
+ for arg in ctx.args:
+ metadata = History(arg)
+ if name not in metadata.symbols:
+ croak("in %s, cannot rename nonexistent %s %s" % (arg, legend, name))
+ if newname in metadata.symbols:
+ croak("in %s, cannot rename to existing %s %s" % (arg, legend, name))
+ ctx = CommandContext(legend, args)
+ if not ctx.is_empty():
+ croak("can't accept a revision-spec when renaming a %s." \
+ % legend)
+ # In the case of a branch, we want to change only the
+ # tag reference.
+ with backend.lifter(arg):
+ backend.set_tag(newname, metadata.symbols[name], metadata)
+ backend.delete_tag(name, metadata)
+ if not quiet and len(args) > 1:
+ announce("in %s, %s -> %s" % (arg, name, newname))
+
+def filecmd(legend, hook, args):
+ CommandContext(legend, args, require_empty=True)
+ if len(args) != 2:
+ croak("%s requires exactly two arguments" % legend)
+ (source, target) = args
+ if not os.path.exists(source):
+ croak("I see no '%s' here." % source)
+ elif os.sep in target and not os.path.exists(os.path.dirname(target)):
+ croak("directory '%s' does not exist." % target)
+ elif not registered(source):
+ croak("%s is not registered" % source)
+ elif registered(target):
+ croak("%s is registered, I won't step on it" % source)
+ elif os.path.exists(target):
+ croak("%s exists, please delete manually if you want it gone" % target)
+ else:
+ hook(source, target)
+
+def move_method(*args):
+ "Move a file and its history."
+ filecmd("move", backend.move, args)
+
+def copy_method(*args):
+ "Copy a file and its history."
+ filecmd("copy", backend.copy, args)
+
+def release_method(*args):
+ "Release locks."
+ ctx = CommandContext("release", args, require_empty=True)
+ for arg in ctx.args:
+ if not os.path.exists(arg):
+ croak("I see no '%s' here." % arg)
+ elif not registered(arg):
+ croak("%s is not registered, skipping" % arg)
+ else:
+ with backend.lifter(arg):
+ backend.release(arg)
+
+def ls_method(*args):
+ "List registered files."
+ if args:
+ croak("ls cannot accept arguments")
+ try:
+ masters = [fn for fn in os.listdir(repodir) if is_history(fn)]
+ except OSError:
+ croak("repo directory %s does not exist" % repodir)
+ masters.sort()
+ for master in masters:
+ sys.stdout.write(backend.workfile(master) + "\n")
+
+def visualize_method(*args):
+ "Generate (and possibly display) a DOT visualization of repo structure."
+ if not os.path.exists(repodir):
+ croak("repository subdirectory %s does not exist" % repodir)
+ args = list(args)
+ comment = CommentContext("visualize", args)
+ ctx = CommandContext("visualize", args, parse_revspec=comment.parse_revspec)
+ for arg in ctx.args:
+ #if not os.path.exists(arg):
+ # croak("I see no '%s' here." % arg)
+ if not registered(arg):
+ croak("%s is not registered." % arg)
+ for arg in ctx.args:
+ metadata = History(arg)
+ ctx.resolve(metadata)
+ if ctx.is_empty():
+ ctx.select_all(metadata)
+ sys.stdout.write("digraph {\n")
+ if len(ctx.args) > 1:
+ pref = arg + ":"
+ else:
+ pref = ""
+ for item in ctx.seq:
+ if item.parent:
+ sys.stdout.write('\t%s%d -> %s%d;\n' \
+ % (pref, item.parent.revno, pref, item.revno))
+ summary = cgi.escape(item.log.split('\n')[0][:42])
+ sys.stdout.write('\t%s%s [shape=box,width=5,label=<<table cellspacing="0" border="0" cellborder="0"><tr><td><font color="blue">%s%s</font></td><td>%s</td></tr></table>>];\n' \
+ % (pref, item.revno, pref, item.revno, summary))
+ if metadata.tip(item.native) == item:
+ sys.stdout.write('\t"%s%s" [shape=oval,width=2];\n' \
+ % (pref, item.branch))
+ sys.stdout.write('\t"%s%s" -> "%s%s" [style=dotted];\n' \
+ % (pref, item.revno, pref, item.branch))
+ keys = sorted(metadata.symbols.keys())
+ for name in keys:
+ native = metadata.symbols[name]
+ branch_label = backend.isbranch(native)
+ if branch_label:
+ native = backend.branch_to_tip(native, metadata)
+ revno = metadata.native_to_revno(native)
+ if not branch_label:
+ sys.stdout.write('\t{rank=same; "%s%s"; "%s%s"}\n' \
+ % (pref, name, pref, revno))
+ sys.stdout.write('\t"%s%s" -> "%s%s" [style=dotted];\n' \
+ % (pref, name, pref, revno))
+ sys.stdout.write("}\n")
+
+def fast_export_method(*args):
+ "Dump revision content to standard output."
+ def attribute(item, who, fallback):
+ s = fallback
+ if item.headers and who in item.headers:
+ s = item.headers[who]
+ t, o = item.unixtime(who)
+ return "%s %s %d %s%02d%02d\n" % \
+ ((who, s, t, "-" if o < 0 else "+") + divmod(abs(o), 3600))
+ ctx = CommandContext("fast-export", args)
+ mark = 0
+ if pseudotime:
+ username = "J. Random Hacker"
+ useremail = "jrh@nowhere.man"
+ else:
+ # Use polystr to ensure that data is decoded to Unicode on
+ # Python 3.
+ username = polystr(capture_or_die("git config --get user.name")).strip()
+ useremail = polystr(capture_or_die("git config --get user.email")).strip()
+ attribution = "%s <%s>" % (username, useremail)
+ for arg in ctx.args:
+ if not registered(arg):
+ croak("%s is not registered" % arg)
+ executable = os.stat(backend.history(arg)).st_mode & stat.S_IXUSR
+ if executable:
+ perms = "100755"
+ else:
+ perms = "100644"
+ metadata = History(arg)
+ ctx.resolve(metadata)
+ if ctx.is_empty():
+ ctx.select_all(metadata)
+ with backend.lifter(arg):
+ markmap = {}
+ last_commit_mark = 0
+ if len(ctx.args) == 1:
+ branch = "master"
+ else:
+ # FIXME: Make this canonical even with tags
+ branch = arg + "/master"
+ oldbranch = branch
+ for c in "~^\\*?":
+ branch = branch.replace(c, "")
+ if not branch:
+ croak("branch name %s is ill-formed" % oldbranch)
+ elif branch != oldbranch:
+ announce("branch name %s sanitized to %s" % (oldbranch, branch))
+ for i in range(ctx.lo, ctx.hi+1):
+ item = metadata.by_revno(i)
+ with backend.cat(arg, item.native) as fp:
+ content = fp.read() # this data will be binary
+ size = len(content) # size will be # of bytes, as desired
+ mark += 1
+ markmap[item.revno] = mark
+ sys.stdout.write("blob\nmark :%d\ndata %d\n" % (mark, size))
+ # Use polystr to ensure sys.stdout gets Unicode in Python 3
+ sys.stdout.write(polystr(content) + "\n")
+ if item.revno == ctx.lo:
+ if ctx.lo == 1:
+ sys.stdout.write("reset refs/heads/%s\n" % branch)
+ else:
+ sys.stdout.write("from refs/heads/%s^0\n" % branch)
+ sys.stdout.write("commit refs/heads/%s\n" % branch)
+ sys.stdout.write("mark :%d\n" % (mark + 1))
+ sys.stdout.write(attribute(item, "author", attribution))
+ sys.stdout.write(attribute(item, "committer", attribution))
+ sys.stdout.write("data %s\n%s" % (len(item.log), item.log))
+ if last_commit_mark:
+ sys.stdout.write("from :%d\n" % last_commit_mark)
+ if len(arg.split()) > 1:
+ arg = '"' + arg + '"'
+ sys.stdout.write("M %s :%d %s\n\n" % (perms, mark, arg))
+ mark += 1
+ last_commit_mark = mark
+ markmap[item.revno] = mark
+ sys.stdout.write("reset refs/heads/%s\nfrom :%d\n\n" % (branch, mark))
+ for (key, val) in list(metadata.symbols.items()):
+ val = metadata.native_to_revno(val)
+ if val in ctx:
+ sys.stdout.write("reset refs/tags/%s\nfrom :%d\n\n" % (key, markmap[val]))
+
+def fast_import_method(*args):
+ "Accept a git fast-import stream on stdin, turn it into file histories."
+ if not isinstance(backend, RCS):
+ croak("fast-import is only supported with the RCS backend")
+ if os.path.exists("RCS"):
+ croak("refusing to unpack into existing RCS directory!")
+ # Force -l to fit SRC's lockless interface.
+ do_or_die(r"rcs-fast-import -l " +" ".join(args), missing="rcs-fast-import")
+ try:
+ os.makedirs(repodir)
+ except OSError:
+ pass
+ for fn in os.listdir("RCS"):
+ corresponding = os.path.join("RCS", os.path.basename(fn))
+ fn = os.path.join(repodir, os.path.basename(fn))
+ if os.path.exists(fn):
+ croak("%s exists, aborting leaving RCS in place!" % corresponding)
+ os.rename(corresponding, fn)
+ shutil.rmtree("RCS")
+
+
+def version_method(*args):
+ "Report SRC's version"
+ sys.stdout.write("src: %s\n" % version)
+ (major, minor, micro, _releaselevel, _serial) = sys.version_info
+ sys.stdout.write("python: %s.%s.%s\n" % (major, minor, micro))
+ sys.stdout.write("%s: %s\n" % (backend.__class__.__name__,backend.version()))
+ sys.stdout.write("platform: %s\n" % sys.platform)
+
+dispatch = {
+ "help": help_method,
+ "commit": commit_method,
+ "ci": commit_method,
+ "add": add_method,
+ "amend": amend_method,
+ "am": amend_method,
+ "list": list_method,
+ "li": list_method,
+ "log": log_method,
+ "checkout": checkout_method,
+ "co": checkout_method,
+ "status": status_method,
+ "st": status_method,
+ "cat": cat_method,
+ "diff": diff_method,
+ "di": diff_method,
+ "tag": tag_method,
+ "branch": branch_method,
+ "rn": rename_method,
+ "rename": rename_method,
+ "ls": ls_method,
+ "move": move_method,
+ "mv": move_method,
+ "copy": copy_method,
+ "cp": copy_method,
+ "visualize": visualize_method,
+ "vis": visualize_method,
+ "fast-export": fast_export_method,
+ "fast-import": fast_import_method,
+ "release": release_method,
+ "version": version_method,
+}
+
+class RevisionMixin:
+ "Express common operations on RCS and SCCS revisions."
+ def splitrev(self, rev):
+ "RCS revision to numeric tuple."
+ return [int(d) for d in rev.split(".")]
+ def joinrev(self, rev):
+ "Numeric tuple to RCS revision."
+ return ".".join([str(d) for d in rev])
+ def pred(self, rev):
+ "Our predecessor. Walks up parent branch."
+ n = self.splitrev(rev)
+ if n[-1] > 1:
+ n[-1] -= 1
+ rev = self.joinrev(n)
+ else:
+ rev = self.joinrev(n[:-2])
+ return rev
+ def succ(self, rev):
+ "Our successor."
+ if rev:
+ n = self.splitrev(rev)
+ n[-1] += 1
+ return self.joinrev(n)
+ else:
+ return "1.1"
+ def isbranch(self, symbol):
+ "Is this a branch symbol?"
+ # RCS convention - this could work for SCCS too, if we simulated
+ # RCS symbols in some way.
+ return "0." in symbol
+ def branch_to_parent(self, revid):
+ "Go from a branch ID sticky tag to the revision it was based on."
+ # Must return an empty string for the fake root sticky tag.
+ return self.joinrev(self.splitrev(revid)[:-2])
+ def branch_to_base(self, revid, metadata):
+ "Go from a branch ID sticky tag to the first revision of its branch."
+ rev = self.branch_to_parent(revid)
+ if rev:
+ rev += ".1.1"
+ else:
+ rev = "1.1"
+ return rev
+ def branch_to_tip(self, revid, metadata):
+ "Go from a branch ID sticky tag to the tip revision of its branch."
+ rev = self.branch_to_base(revid, metadata)
+ while True:
+ nxt = self.succ(rev)
+ if metadata.native_to_revno(nxt) is None:
+ return rev
+ else:
+ rev = nxt
+ croak("internal error: couldn't find branch tip of %s" % rev)
+ def branch_stickybase(self, name, revision, metadata):
+ "Compute a base and tip revision for a named branch."
+ if name in metadata.symbols:
+ # Must unstickify...
+ base = metadata.symbols[name].split(".")
+ base = base[:-2] + base[-1:]
+ base = ".".join(base)
+ return(None, base)
+ else:
+ def branchfrom(c, p):
+ "Is c a branch child (not direct descendant) of parent p?"
+ c = c.split(".")
+ p = p.split(".")
+ return len(c) == len(p) + 2 and c[len(p):] == p
+ baserev = metadata.current()
+ newsib = len([item for item in metadata.revlist \
+ if branchfrom(item.native, baserev)])
+ newsib += 1
+ base = baserev + "." + str(newsib)
+ sticky = baserev + ".0." + str(newsib)
+ return (sticky, base)
+ def branch_initial(self, branchname, metadata):
+ "Return initial commit of a named branch (the child of the root)."
+ # What we need to do is find the branch tip, then walk back to
+ # just after the first point where it joins another branch, then
+ # do a delete to end of branch from there.
+ base = self.branch_to_tip(metadata.symbols[branchname], metadata)
+ while True:
+ if base is not None and metadata.by_native_d[base].branches:
+ break
+ else:
+ base = self.pred(base)
+ return self.succ(base)
+
+class RCS(RevisionMixin):
+ "Encapsulate RCS back end methods."
+ delimiters = ('----------------------------',
+ '=============================================================================')
+ class RCSLifter:
+ "Temporarily lift a master to the working directory."
+ def __init__(self, name):
+ self.name = name
+ self.where = os.getcwd()
+ self.deferred = []
+ self.previous_handlers = {}
+ def defer_signal(self, sig_num, stack_frame):
+ self.deferred.append(sig_num)
+ def __enter__(self):
+ # Replace existing signal handlers with deferred handler
+ for sig_num in (signal.SIGHUP, signal.SIGINT, signal.SIGTERM):
+ # signal.signal returns None when no handler has been
+ # set in Python, which is the same as the default
+ # handler (SIG_DFL) being set
+ self.previous_handlers[sig_num] = (
+ signal.signal(sig_num, self.defer_signal) or signal.SIG_DFL)
+ if os.path.dirname(self.name):
+ os.chdir(os.path.dirname(self.name))
+ do_or_die("mv '{0}'/'{1},v' .".format(repodir, self.name))
+ def __exit__(self, extype, value, traceback_unused):
+ os.chdir(self.where)
+ if extype and debug > 0:
+ raise extype(value)
+ do_or_die("mv '{0},v' '{1}'".format(self.name, repodir))
+ # Restore handlers
+ for sig_num, handler in self.previous_handlers.items():
+ signal.signal(sig_num, handler)
+ # Send deferred signals
+ while self.deferred:
+ sig_num = self.deferred.pop(0)
+ os.kill(os.getpid(), sig_num)
+ return True
+ def lifter(self, name):
+ return RCS.RCSLifter(name)
+ def history(self, arg):
+ return os.path.join(os.path.dirname(arg),
+ repodir,
+ os.path.basename(arg) + ",v")
+ @staticmethod
+ def is_history(path):
+ return path.endswith(",v")
+ def has_revisions(self, arg):
+ "Does the master for this file have any revisions"
+ # The magic number 105 is the size of an empty RCS file (no
+ # metadata, no revisions) at 76 bytes, plus 29 bytes. We assume
+ # that this size has stayed constant or increased since ancient
+ # times. In fact the size of an RCS file with revisions goes up
+ # more - by the 78 bytes for the final, fixed line of the log
+ # display. This gives us plenty of slack to cope with minor
+ # format differences.
+ #
+ # The motivation here is to make "src status" faster by avoiding
+ # the need for an entire log parse when checking for "A" status.
+ return os.path.getsize(self.history(arg)) > 105
+ def workfile(self, arg):
+ "Workfile corresponding to an RCS master"
+ return arg[:-2]
+ def register(self, arg):
+ "Register a file"
+ # Key choices here: -b suppresses all keyword expansion, -U sets
+ # non-strict locking (which makes branch appends less painful).
+ do_or_die("rcs -q -U -kb -i '{0}' </dev/null && mv '{0},v' '{1}'".format(arg, repodir))
+ def checkin(self, arg, comment):
+ "Check in a commit, with comment."
+ comment = "'" + comment.replace("'", r"'\''") + "'"
+ # By unlocking the file before checkin we invoke the following
+ # property described on the rcs(1) manual page: "If rev is
+ # omitted and the caller has no lock, but owns the file and
+ # locking is not set to strict, then the revision is appended to
+ # the default branch (normally the trunk; see the -b option of
+ # rcs(1))." This is the behavior we want and why locking is set
+ # to non-strict.
+ do_or_die("rcs -q -U -u '{0},v' && ci -l -m{1} '{0}'".format(arg, comment))
+ def checkout(self, arg, revision):
+ "Check out a revision. Leaves it writeable."
+ do_or_die("rm -f '{0}' && rcs -q -u '{0},v' && co -q -l{1} '{0}'".format(arg, revision))
+ def amend(self, arg, rev, comment):
+ "Amend a commit comment."
+ # Relies on caller to escape comment string
+ comment = "'" + comment.replace("'", r"'\''") + "'"
+ do_or_die("rcs -m{0}:{1} '{2}'".format(rev, comment, arg))
+ def cat(self, arg, revision):
+ "Ship the contents of a revision to stdout or a named file."
+ return popen_or_die("co -q -p -r{1} '{0}'".format(arg, revision), "r")
+ def delete_tag(self, tagname, metadata):
+ "Delete a specified tag."
+ do_or_die("rcs -n{0} '{1}'".format(tagname, metadata.name))
+ def set_tag(self, tagname, revision, metadata):
+ "Set a specified tag."
+ do_or_die("rcs -n{0}:{1} '{2}'".format(tagname, revision, metadata.name))
+ def delete_branch(self, branchname, metadata):
+ "Delete a specified branch."
+ # From rcs(1): -orange deletes ("outdates") the revisions given
+ # by range. A range consisting of a single revision number
+ # means that revision. A range consisting of a branch number
+ # means the latest revision on that branch. A range of the form
+ # rev1:rev2 means revisions rev1 to rev2 on the same branch,
+ # :rev means from the beginning of the branch containing rev
+ # up to and including rev, and rev: means from revision rev to
+ # the end of the branch containing rev. None of the outdated
+ # revisions can have branches or locks.
+ do_or_die("rcs -o{0}: '{1}'".format(self.branch_initial(branchname, metadata), metadata.name))
+ self.delete_tag(branchname, metadata)
+ def set_branch(self, name, revision, metadata):
+ "Set the specified branch to be default, creating it if required."
+ (sticky, base) = self.branch_stickybase(name, revision, metadata)
+ if sticky:
+ do_or_die("rcs -n{0}:{1} '{2}'".format(name, sticky, metadata.name))
+ do_or_die("rcs -b%s %s" % (base, metadata.name))
+ def move(self, source, target):
+ "Move a file and its history."
+ do_or_die("mv '{0}' '{1}' && mv '{2}' '{3}'".format(source, target,
+ self.history(source),
+ self.history(target)))
+ def copy(self, source, target):
+ "Copy a file and its history."
+ do_or_die("cp '{0}' '{1}' && cp '{2}' '{3}'".format(source, target,
+ self.history(source),
+ self.history(target)))
+ def release(self, arg):
+ "Release locks."
+ do_or_die("rcs -q -u '{0},v'".format(arg))
+ def write_description(self, text, metadata):
+ "Write description field. If text is empty, clear the field."
+ text = "'" + text.replace("'", r"'\''") + "'"
+ do_or_die("rcs -t-{1} '{0}'".format(metadata.name, text))
+ def version(self):
+ rawversion = capture_or_die("rcs --version")
+ m = re.search(r"[0-9]+\.[0-9]+\.[0-9]+".encode('ascii'),rawversion)
+ return m and polystr(m.group(0))
+ def parse(self, metadata):
+ "Get and parse the RCS log output for this file."
+ def sortkey(rev):
+ return rev.date + \
+ '.'.join('%010d' % int(n) for n in rev.native.split('.'))
+ metadata.symbols["trunk"] = "0.1"
+ with popen_or_die("cd %s >/dev/null; rlog %s 2>/dev/null; cd ..>/dev/null" % (repodir, metadata.name)) as fp:
+ if debug >= DEBUG_PARSE:
+ sys.stderr.write("\t-> init\n")
+ state = "init"
+ for line in fp:
+ # All operations here will need Unicode in Python 3
+ line = polystr(line)
+ if debug >= DEBUG_PARSE:
+ sys.stderr.write("in: %s\n" % repr(line))
+ if state == "init":
+ if line.startswith("locks:"):
+ if debug >= DEBUG_PARSE:
+ sys.stderr.write("\t-> locks\n")
+ state = "locks"
+ elif line.startswith("symbolic names:"):
+ if debug >= DEBUG_PARSE:
+ sys.stderr.write("\t-> symbols\n")
+ state = "symbols"
+ elif line.startswith("branch:"):
+ branch = line.split(":")[1].strip()
+ # Undocumented fact about RCS: The branch "1"
+ # is the same as the blank branch. Significant
+ # because you can't reset to the blank branch
+ # using rcs -b, that resets to the dynamically
+ # highest branch.
+ if not branch or branch == "1":
+ metadata.branch = "trunk"
+ elif line.startswith("description:"):
+ state = "description"
+ elif state == "description":
+ if line.startswith("============================"):
+ break
+ elif line.startswith("----------------------------"):
+ if debug >= DEBUG_PARSE:
+ sys.stderr.write("\t-> logheader\n")
+ state = "logheader"
+ metadata.revlist.append(HistoryEntry(metadata))
+ metadata.description = metadata.description[:-1]
+ else:
+ metadata.description += line
+ elif state == "locks":
+ if not line[0].isspace():
+ if debug >= DEBUG_PARSE:
+ sys.stderr.write("\t-> init\n")
+ state = "init"
+ else:
+ fields = line.strip().split()
+ metadata.lockrevs.append(fields[1])
+ elif state == "symbols":
+ if not line[0].isspace():
+ if debug >= DEBUG_PARSE:
+ sys.stderr.write("\t-> init\n")
+ state = "init"
+ else:
+ fields = line.strip().split()
+ name = fields[0]
+ if name.endswith(":"):
+ name = name[:-1]
+ rev = fields[1]
+ metadata.symbols[name] = rev
+ elif state == "logheader":
+ if line.startswith("revision "):
+ fields = line.split()
+ metadata.revlist[-1].native = fields[1]
+ elif line.startswith("----------------------------"):
+ metadata.revlist.append(HistoryEntry(metadata))
+ elif line.startswith("date: "):
+ fields = line.split()
+ date = fields[1] + " " + fields[2]
+ if date.endswith(";"):
+ date = date[:-1]
+ date = date.replace("/","-").replace(" ","T") + "Z"
+ metadata.revlist[-1].date = date
+ elif line.startswith("branches:"):
+ continue
+ elif line.startswith("======================="):
+ # This deals with RCS v5.7 issuing a log header
+ # divider just before the terminator, something
+ # v5.8 does not do.
+ if not metadata.revlist[-1].native:
+ metadata.revlist.pop()
+ elif not metadata.revlist[-1].log.endswith("\n"):
+ metadata.revlist[-1].log += "\n"
+ break
+ elif line.strip() == "*** empty log message ***":
+ continue
+ elif metadata.revlist:
+ metadata.revlist[-1].log += line
+ # Now that we have the symbol table...
+ if metadata.branch != "trunk":
+ for (k, v) in list(metadata.symbols.items()):
+ if v == metadata.branch:
+ metadata.branch = k
+ break
+ else:
+ croak("unrecognized branch ID '%s'" % branch)
+ metadata.revlist.sort(key=sortkey)
+ for (i, item) in enumerate(metadata.revlist):
+ if pseudotime:
+ # Artificial date one day after the epoch
+ # to avoid timezone issues.
+ item.date = rfc3339(86400 + i * 60)
+ item.revno = i + 1
+ metadata.revlist.reverse()
+ if debug >= DEBUG_PARSE:
+ #sys.stdout.write("\t%d revisions\n" % len(metadata.revlist)
+ sys.stdout.write("\tlockrevs: %s\n" % metadata.lockrevs)
+ sys.stdout.write("\tsymbols: %s\n" % metadata.symbols)
+ metadata.build_indices()
+
+# SCCS branch numbering (not supported yet) works differently from RCS
+# branch numbering. Node IDs have at most 4 parts in the form R.B.L.S
+# (release, level, branch, sequence):
+#
+# 1.0 - initial trunk version
+# 1.1 - next checkin on trunk
+# 1.1.1.0 - First branch from trunk v1.1
+# 1.1.1.1 - Next checkin on that branch
+# 1.1.2.0 - Second branch from v1.1
+# 1.2 - next checkin on trunk.
+#
+# The main difference is that you cannot branch from a branch, only from
+# trunk.
+
+class SCCS(RevisionMixin):
+ "Encapsulate SCCS back end methods."
+ delimiters = ("-X-X-X-X-X--X-X-X-X-X--X-X-X-X-X-",)
+ class SCCSLifter:
+ "Temporarily lift a master to the working directory."
+ def __init__(self, name):
+ self.name = name
+ self.where = os.getcwd()
+ if not os.path.isdir("SCCS"):
+ croak("no SCCS directory.")
+ self.deferred = []
+ self.previous_handlers = {}
+ def defer_signal(self, sig_num, stack_frame):
+ self.deferred.append(sig_num)
+ def __enter__(self):
+ # Replace existing signal handlers with deferred handler
+ for sig_num in (signal.SIGHUP, signal.SIGINT, signal.SIGTERM):
+ # signal.signal returns None when no handler has been
+ # set in Python, which is the same as the default
+ # handler (SIG_DFL) being set
+ self.previous_handlers[sig_num] = (
+ signal.signal(sig_num, self.defer_signal) or signal.SIG_DFL)
+ def __exit__(self, extype, value, traceback_unused):
+ # Restore handlers
+ for sig_num, handler in self.previous_handlers.items():
+ signal.signal(sig_num, handler)
+ # Send deferred signals
+ while self.deferred:
+ sig_num = self.deferred.pop(0)
+ os.kill(os.getpid(), sig_num)
+ return True
+ def lifter(self, name):
+ return SCCS.SCCSLifter(name)
+ def __sccsfile(self, arg, pref):
+ return os.path.join(os.path.dirname(arg),
+ "SCCS",
+ pref + "." + os.path.basename(arg))
+ def history(self, arg):
+ return self.__sccsfile(arg, "s")
+ @staticmethod
+ def is_history(path):
+ return path.startswith("s.")
+ def has_revisions(self, arg):
+ "Does the master for this file have any revisions"
+ # It's not possible to create an SCCS file without at least one
+ # revision.
+ return True
+ def workfile(self, arg):
+ "Workfile corresponding to an SCCS master"
+ return arg[2:]
+ def register(self, arg):
+ "Register a file"
+ if not os.path.isdir("SCCS"):
+ os.mkdir("SCCS")
+ def checkin(self, arg, comment):
+ "Check in a commit, with comment."
+ comment = "'" + comment.replace("'", r"'\''") + "'"
+ if os.path.exists(self.history(arg)):
+ cmd = "delta -s -y{1} {0}".format(arg, comment)
+ else:
+ # Yuck - 2>/dev/null is required to banish the message
+ # admin: warning: SCCS/XXXXX: No id keywords.
+ cmd = "admin -fb -i '{0}' -y{1} <'{0}' 2>/dev/null".format(arg, comment)
+ do_or_die("TZ=UTC sccs " + cmd)
+ do_or_die("rm -f {0} && sccs get -e -s '{0}' >/dev/null".format(arg))
+ def checkout(self, arg, revision):
+ "Check out a revision. Leaves it writeable."
+ do_or_die("rm -f SCCS/p.{0}".format(arg))
+ if revision:
+ do_or_die("rm -f '{0}' && sccs get -s -e -r{1} '{0}' >/dev/null".format(arg, revision))
+ else:
+ do_or_die("rm -f '{0}' && sccs get -s -e '{0}' >/dev/null".format(arg))
+ def amend(self, arg, rev, comment):
+ "Amend a commit comment."
+ comment = "'" + comment.replace("'", r"'\''") + "'"
+ do_or_die("sccs cdc -r{1} -y{2} '{0}'".format(arg, rev, comment))
+ def cat(self, arg, revision):
+ "Ship the contents of a revision to stdout or a named file."
+ if revision:
+ return popen_or_die("sccs get -s -p -r{1} '{0}' 2>/dev/null".format(arg, revision), "r")
+ else:
+ return popen_or_die("sccs get -p {0}".format(arg), "r")
+ def delete_tag(self, tagname, metadata):
+ "Delete a specified tag."
+ croak("tags are not supported in the SCCS back end")
+ def set_tag(self, tagname, revision, metadata):
+ "Set a specified tag."
+ croak("tags are not supported in the SCCS back end")
+ def delete_branch(self, branchname, metadata):
+ "Delete a specified branch."
+ croak("branches are not supported in the SCCS back end")
+ def set_branch(self, name, revision, metadata):
+ "Set the specified branch to be default, creating it if required."
+ croak("branches are not supported in the SCCS back end")
+ def move(self, source, target):
+ "Move a file and its history."
+ do_or_die("mv '{0}' '{1}' && mv '{2}' '{3}'".format(source, target,
+ self.history(source),
+ self.history(target)))
+ def copy(self, source, target):
+ "Copy a file and its history."
+ do_or_die("cp '{0}' '{1}' && cp '{2}' '{3}'".format(source, target,
+ self.history(source),
+ self.history(target)))
+ def release(self, arg):
+ "Release locks."
+ do_or_die("cssc admin -dla '{0}'".format(arg))
+ def write_description(self, text, metadata):
+ "Write description field. If text is empty, clear the field."
+ if text:
+ scribblefile = os.path.join(os.path.dirname(metadata.name),"SCCS",".scribble")
+ # Write to filesystem as binary ASCII data for robustness
+ with open(scribblefile, "wb") as wfp:
+ wfp.write(polybytes(text) + b"\n")
+ else:
+ scribblefile = ''
+ do_or_die("sccs admin -t{0} {1} 2>/dev/null".format(scribblefile, metadata.name))
+ if text:
+ os.remove(scribblefile)
+ def version(self):
+ rawversion = capture_or_die("sccs --version 2>&1")
+ m = re.search(r"[0-9]+\.[0-9]+\.[0-9]+".encode('ascii'), rawversion)
+ return m and polystr(m.group(0))
+ def parse(self, metadata):
+ "Get and parse the SCCS log output for this file."
+ metadata.symbols["trunk"] = "0.1"
+ with popen_or_die("sccs prs -e -d':FD::Dt:\n:C:{1}' {0} 2>/dev/null".format(metadata.name, SCCS.delimiters[0])) as fp:
+ if debug >= DEBUG_PARSE:
+ sys.stderr.write("\t-> init\n")
+ state = "init"
+ for line in fp:
+ # All operations here will need Unicode in Python 3
+ line = polystr(line)
+ if debug >= DEBUG_PARSE:
+ sys.stderr.write("in %s: %s\n" % (state, repr(line)))
+ if state == 'init':
+ if line == 'none\n':
+ line = ''
+ metadata.description = line
+ state = 'header'
+ elif state == "header":
+ comment = ""
+ if line.startswith("D "):
+ try:
+ (_, rev, yymmdd, hhmmss) = line.split()[:4]
+ metadata.revlist.append(HistoryEntry(metadata))
+ metadata.revlist[-1].native = rev
+ yymmdd = CENTURY + yymmdd.replace('/','-')
+ metadata.revlist[-1].date = yymmdd+"T"+hhmmss+"Z"
+ except ValueError:
+ croak("ill-formed delta line")
+ if debug >= DEBUG_PARSE:
+ sys.stderr.write("\t-> header\n")
+ state = "comment"
+ elif state == "comment":
+ if line == SCCS.delimiters[0] + "\n":
+ metadata.revlist[-1].log = comment.rstrip() + "\n"
+ if debug >= DEBUG_PARSE:
+ sys.stderr.write("\t-> init\n")
+ state = "header"
+ else:
+ comment += line
+ metadata.revlist.sort(key=lambda x: x.date)
+ for (i, item) in enumerate(metadata.revlist):
+ if pseudotime:
+ # Artificial date one day after the epoch
+ # to avoid timezone issues.
+ item.date = rfc3339(86400 + i * 60)
+ item.revno = i + 1
+ metadata.revlist.reverse()
+ try:
+ with open(self.__sccsfile(metadata.name, "p"), "rb") as fp:
+ for line in fp:
+ metadata.lockrevs.append(polystr(line).split()[0])
+ except IOError:
+ pass
+ metadata.build_indices()
+
+backends = (RCS, SCCS)
+
+if __name__ == "__main__":
+ try:
+ commandline = list(sys.argv[1:])
+ explicit = False
+ repodir = ".src"
+ backend = RCS
+
+ if not os.path.exists(".src"):
+ for vcs in backends:
+ if os.path.exists(vcs.__name__):
+ repodir = vcs.__name__
+ backend = vcs
+ break
+
+ while commandline and commandline[0].startswith("-"):
+ if commandline[0] == '-d':
+ debug += 1
+ elif commandline[0] == '-q':
+ quiet = True
+ elif commandline[0] == '-T':
+ pseudotime = True
+ elif commandline[0] == '-S':
+ repodir = commandline[1]
+ explicit = True
+ commandline.pop(0)
+ else:
+ croak("unknown option %s before command verb" % commandline[0])
+ commandline.pop(0)
+
+ if not commandline:
+ help_method()
+ raise SystemExit(0)
+
+ # User might want to force the back end
+ for vcs in backends:
+ if commandline[0] == vcs.__name__.lower():
+ backend = vcs
+ commandline.pop(0)
+ break
+
+ # Ugly constraint...
+ if backend.__name__ == 'SCCS':
+ repodir = "SCCS"
+
+ backend = backend()
+
+ if not commandline:
+ help_method()
+ raise SystemExit(0)
+
+ if commandline[0] in dispatch:
+ dispatch[commandline[0]](*commandline[1:])
+ else:
+ croak("no such command as '%s'. Try 'src help'" \
+ % commandline[0])
+ except KeyboardInterrupt:
+ pass
+
+# The following sets edit modes for GNU EMACS
+# Local Variables:
+# mode:python
+# End:
diff --git a/src.1 b/src.1
new file mode 100644
index 0000000..ed4b30a
--- /dev/null
+++ b/src.1
@@ -0,0 +1,311 @@
+'\" t
+.\" Title: src
+.\" Author: [FIXME: author] [see http://docbook.sf.net/el/author]
+.\" Generator: DocBook XSL Stylesheets v1.79.1 <http://docbook.sf.net/>
+.\" Date: 10/28/2018
+.\" Manual: \ \&
+.\" Source: \ \&
+.\" Language: English
+.\"
+.TH "SRC" "1" "10/28/2018" "\ \&" "\ \&"
+.\" -----------------------------------------------------------------
+.\" * Define some portability stuff
+.\" -----------------------------------------------------------------
+.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+.\" http://bugs.debian.org/507673
+.\" http://lists.gnu.org/archive/html/groff/2009-02/msg00013.html
+.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+.ie \n(.g .ds Aq \(aq
+.el .ds Aq '
+.\" -----------------------------------------------------------------
+.\" * set default formatting
+.\" -----------------------------------------------------------------
+.\" disable hyphenation
+.nh
+.\" disable justification (adjust text to left margin only)
+.ad l
+.\" -----------------------------------------------------------------
+.\" * MAIN CONTENT STARTS HERE *
+.\" -----------------------------------------------------------------
+.SH "NAME"
+src \- simple revision control
+.SH "SYNOPSIS"
+.sp
+\fBsrc\fR [command] [revision\-spec] [\fIfile\fR\&...]
+.SH "DESCRIPTION"
+.sp
+SRC (or src) is simple revision control, a version\-control system for single\-file projects by solo developers and authors\&. It modernizes the venerable RCS, hence the anagrammatic acronym\&. The design is tuned for use cases like all those little scripts in your "~/bin" directory, or a directory full of single\-file HOWTOs\&.
+.sp
+SRC revision histories are single, human\-readable files beneath a hidden "\&.src" subdirectory in the directory where they live\&. There may be multiple histories under one directory; SRC treats them as separate projects, and history files can be moved elsewhere at any time\&.
+.sp
+SRC gives you simple, consecutive integer revision numbers\&. It supports tags and branching\&. It does not show committer information, because the committer is always you\&. The command set is intended to look familiar if you have ever used Subversion, Mercurial, or Git\&.
+.sp
+SRC interprets the EDITOR variable in the usual way, using it to spawn an editor instance when you perform a commit or amend\&.
+.sp
+SRC is fully supported in Emacs VC mode\&.
+.SH "COMMAND SUMMARY"
+.sp
+A "revision" is a 1\-origin integer, or a tag name designating an integer revision, or a branch name designating the tip revision of its branch\&. Revision numbers always increase in commit\-date order\&.
+.sp
+A revision range is a single revision, or a pair of revisions "M\-N" (all revisions numerically from M to N) or "M\&.\&.N" (all revisions that are branch ancestors of N and branch successors of M)\&.
+.sp
+If SRC complains that your revision spec looks like a nonexistent filename, you can prefix it with "@" (this is always allowed)\&. "@" by itself means the current (checked\-out) revision\&.
+.sp
+Unless otherwise noted under individual commands, the default revision is the tip revision on the current branch and the default range is all revisions on the current branch\&.
+.sp
+The token "\-\-" tells the command\-line interpreter that subcommands, switches, and revision\-specs are done \- everything after it is a filename, even if it looks like a subcommand or revision number\&.
+.PP
+src help [\fIcommand\fR]
+.RS 4
+Displays help for commands\&.
+.RE
+.PP
+src commit [\-|\-m \fIstring\fR|\-f \fIfile\fR|\-e] [\fIfile\fR\&...]
+.RS 4
+Enters a commit for specified files\&. Separately to each one\&. A history is created for the file if it does not already exist\&. With
+\fI\-\fR, take comment text from stdin; with
+\fI\-m\fR
+use the following string as the comment; with
+\fI\-f\fR
+take from a file\&. With
+\fI\-e\fR, edit even after
+\fI\-\fR,
+\fI\-f\fR
+or
+\fI\-m\fR\&. "ci" is a synonym for "commit"\&.
+.RE
+.PP
+src amend [\-|\-m \fIstring\fR|\-f \fIfile\fR|\-e] [\fIrevision\fR] [\fIfile\fR\&...]
+.RS 4
+Amends the stored comment for a specified revision, defaulting to the latest revision on the current branch\&. Flags are as for commit\&. "am" is a synonym for "amend"\&.
+.RE
+.PP
+src checkout [\fIrevision\fR] [\fIfile\fR\&...]
+.RS 4
+Refresh the working copy of the file(s) from their history files\&. "co" is a synonym for "checkout"\&.
+.RE
+.PP
+src cat [\fIrevision\fR] [\fIfile\fR\&...]
+.RS 4
+Send the specified revision of the files to standard output\&.
+.RE
+.PP
+src status [\-a] [\fIfile\fR\&...]
+.RS 4
+"A" = added, "=" = unmodified, "M" = modified, "!" = missing, "?" = not tracked, "I" = ignored, "L" = locked (recover with "src checkout")\&. The "A" and "L" statuses should only occur if you have used RCS directly on a file\&. Normally "?" and "I" files are not listed; this changes if you either give the
+\fI\-a\fR
+switch or specify which files to look at\&. "st" is a synonym for "status"\&.
+.RE
+.PP
+src tag [list|\-l|create|\-c|delete|del|\-d] [\fIname\fR] [\fIrevision\fR] [\fIfile\fR\&...]
+.RS 4
+List tags, create tags, or delete tags\&. Create/delete takes a revision, defaulting to the current branch tip\&. List to all revisions\&.
+.RE
+.PP
+src branch [list|\-l|create|\-c|delete|del|\-d] [\fIname\fR] [\fIfile\fR\&...]
+.RS 4
+List, create, or delete branches\&. When listing, the active branch is first in the list\&. The default branch is "trunk"\&. Create/delete takes a singleton revision, defaulting to the current branch tip\&. List defaults to all revisions, including 0 (the trunk root phantom revision)\&.
+.RE
+.PP
+src rename [\fItag\fR|\fIbranch\fR] [\fIoldname\fR] [\fInewname\fR] [\fIfile\fR\&...]
+.RS 4
+Rename a tag or branch\&. Refuses to step on an existing symbol or rename a nonexistent one\&. "rn" is a synonym for "rename"\&.
+.RE
+.PP
+src list [(\-<n>|\-l <n>)] [\-f \fIfmt\fR] [\fIrevision\-range\fR] [\fIfile\fR\&...]
+.RS 4
+Sends summary information about the specified commits to standard output\&. The summary line tagged with "*" is the state that the file would return to on checkout without a revision\-spec\&. See
+\fIhelp list\fR
+for information about custom formats\&. Use
+\fI\-<n>\fR
+or
+\fI\-l <n>\fR, where <n> is a number, to limit the listing length\&. Default range is the current branch, reversed\&. "li" is a synonym for "list"\&.
+.RE
+.PP
+src log [\-v] [(\-<n>|\-l <n>)] [(\-p|\-u|\-c) [\-b|\-w]] [\fIrevision\-range\fR] [\fIfile\fR\&...]
+.RS 4
+Sends log information about the specified commits to standard output\&. Use
+\fI\-<n>\fR
+or
+\fI\-l <n>\fR, where <n> is a number, to limit the listing length\&. Default range is the current branch, reversed\&. The
+\fI\-\-patch\fR,
+\fI\-p\fR
+or
+\fI\-u\fR
+option additionally sends a unified format diff listing to standard output for each revision against its immediate ancestor revision;
+\fI\-c\fR
+emits a context diff instead\&. When generating a diff,
+\fI\-b\fR
+ignores changes in whitespace, and
+\fI\-w\fR
+ignores all whitespace\&. Histories imported via
+\fIfast\-import\fR
+(when not using its
+\fI\-p\fR
+option) have RFC\-822\-style headers inserted into the log comment to preserve metadata not otherwise representable in SRC, such as distinct author and committer identifications and dates\&. These headers are normally suppressed by
+\fIlog\fR, however,
+\fI\-v\fR
+shows a summarized view of important headers;
+\fI\-v \-v\fR
+shows all headers as\-is\&.
+.RE
+.PP
+src diff [(\-u|\-c) [\-b|\-w]] [\fIrevision\-range\fR] [\fIfile\fR\&...]
+.RS 4
+Sends a diff listing to standard output\&. With no revision spec, diffs the working copy against the last version checked in\&. With one revno, diffs the working copy against that stored revision; with a range, diff between the beginning and end of the range\&. The actual difference generation is done with diff(1)\&. The default diff format is
+\fI\-u\fR
+(unified), but if you specify a
+\fI\-c\fR
+option after the verb a context diff will be emitted\&.
+\fI\-b\fR
+ignores changes in the amount of whitespace, and
+\fI\-w\fR
+ignores all whitespace\&. "di" is a synonym for "diff"\&.
+.RE
+.PP
+src ls
+.RS 4
+List all registered files\&.
+.RE
+.PP
+src visualize
+.RS 4
+Emit a DOT visualization of repository structures\&. To use this, install the graphviz package and pipe the output to something like "dot \-Tpng | display \-"\&. "vis" is a synonym for "visualize"\&.
+.RE
+.PP
+src move \fIold\fR \fInew\fR
+.RS 4
+Rename a workfile and its history\&. Refuses to step on existing workfiles or histories\&. "mv" is a synonym for "move"\&.
+.RE
+.PP
+src copy \fIold\fR \fInew\fR
+.RS 4
+Copy a file and its master\&. Refuses to step on existing files or masters\&. "cp" is a synonym for "copy"\&.
+.RE
+.PP
+src fast\-export [\fIrevision\-range\fR] [\fIfile\fR\&...]
+.RS 4
+Export one or more projects as a Git fast\-import stream\&. For a history originally imported from elsewhere, author and committer identification is gleaned from the RFC\-822\-style headers inserted into the commit comment by
+\fIfast\-import\fR
+(if its
+\fI\-p\fR
+option was not used)\&. Otherwise, this information is copied from your Git configuration\&.
+.RE
+.PP
+src fast\-import [\-p] [\fIfiles\fR\&...]
+.RS 4
+Parse a git\-fast\-import stream from standard input\&. The modifications for each individual file become separate SRC histories\&. Mark, committer and author data, and mark cross\-references to parent commits, are preserved in RFC\-822\-style headers on log comments unless the
+\fI\-p\fR
+(plain) option is given, in which case this metadata is discarded\&. Give arguments to restrict the files imported\&.
+.RE
+.PP
+src release [\fIfile\fR\&...]
+.RS 4
+Release locks on files\&. This is never necessary in a normal workflow, which will be repeated edit\-commit cycles, but it may be handy if you have to interoperate with other tools that expect RCS masters to be in their normal (unlocked) state\&.
+.RE
+.PP
+src version
+.RS 4
+Report the versions of SRC, the underlying Python, and the back end\&.
+.RE
+.sp
+The omission of "src remove" is a deliberate speed bump\&.
+.sp
+If no files are specified, all eligible files are operated on in sequence\&.
+.SH "NOISE CONTROL"
+.sp
+Silence is golden\&. When you have selected only one file to operate on, and the command is not a report generator (\fIstatus\fR, \fIcat\fR, \fIlog\fR, \fIlist\fR, \fIfast\-export\fR, the listing modes of \fItag\fR and \fIbranch\fR, \fIls\fR) you will see a reply only if the operation failed\&.
+.sp
+Other commands (\fIcommit\fR, \fIcheckout\fR, tag creation and deletion) give you a success message per file when operating on multiple files, so you will not be in doubt about which operation succeeded\&. This behavior can be suppressed with the \fI\-q\fR option, placed \fIbefore\fR the subcommand word\&.
+.sp
+If your directory contains a file named "\&.srcignore", each line that is neither blank nor begins with a "#" is interpreted as an ignore pattern\&. It is expanded with glob(3), and files in the expansion are omitted from \fIsrc status\fR \- unless the file is named as an argument, of the status command, in which case its status is "I"\&. Thus, for example, a line reading "*\&.html" will cause all files with an HTML extension to be omitted from the output of "src status", but the output of \fIsrc status *\fR will list them with status "I"\&.
+.SH "BACKWARD COMPATIBILITY"
+.sp
+SRC history files are (normally) RCS master files\&. SRC maintains no permanent state other than these files\&.
+.sp
+SRC takes over the little\-used "description" field of RCS (and SCCS) master files to store some state that makes status checks faster\&. If you try to use SRC to edit a pre\-existing RCS\- or SCCS\-registered file with a non\-empty description field, SRC will refuse to step on the old description; you must clear it manually\&.
+.sp
+In order to maintain complete backwards compatibility, one other compromise was made: any commit comment containing a string exactly matching an RCS log delimiter (a long string of "\-" characters) will be rejected as malformed\&.
+.sp
+The RCS back end will be automatically selected when there is an "\&.src" or "RCS" subdirectory\&.
+.sp
+You can explicitly select the RCS back end by making the first command keyword on the src command line be \fIrcs\fR\&. This should only be necessary when your working directory contains two or more of the subdirectories "\&.src", "RCS", and "SCCS"\&.
+.sp
+By default, history files are kept in a hidden subdirectory named "\&.src"\&. But if you have an RCS subdirectory and no "\&.src", SRC will quietly operate on the files in the RCS directory in a completely backward\-compatible way\&.
+.SH "WORKING WITH SCCS"
+.sp
+Using SCCS as a back end is also supported, with some limits due to missing features in SCCS:
+.sp
+.RS 4
+.ie n \{\
+\h'-04'\(bu\h'+03'\c
+.\}
+.el \{\
+.sp -1
+.IP \(bu 2.3
+.\}
+all commands relating to tags and branches throw an error\&.
+.RE
+.sp
+.RS 4
+.ie n \{\
+\h'-04'\(bu\h'+03'\c
+.\}
+.el \{\
+.sp -1
+.IP \(bu 2.3
+.\}
+src cat does not necessarily pipe out binary data correctly\&.
+.RE
+.sp
+.RS 4
+.ie n \{\
+\h'-04'\(bu\h'+03'\c
+.\}
+.el \{\
+.sp -1
+.IP \(bu 2.3
+.\}
+the exec bit is not propagated between master and workfile\&.
+.RE
+.sp
+The SCCS back end will be automatically selected when there is an "SCCS" subdirectory and no "\&.src" or "RCS" subdirectory\&.
+.sp
+You can explicitly select the SCCS back end by making the first command keyword on the src command line be \fIsccs\fR\&. This should only be necessary when your working directory contains none or two or more of the subdirectories "\&.src", "RCS", and "SCCS"\&.
+.sp
+Working with SCCS requires an "SCCS" subdirectory; SRC will quietly create one, if required, then operate on the files in the "SCCS" directory in a completely backward\-compatible way\&.
+.sp
+Fast\-import to SCCS is not supported\&.
+.sp
+The SCCS mode is not recommended unless you have a specific need to work with legacy SCCS repositories\&. Up\-converting them to something less Paleolithic would be a better idea; in truth, the SCCS support exists mainly because it\(cqs hilarious (for obscure hacker values of \fIhilarious\fR)\&.
+.SH "DEBUGGING OPTIONS"
+.sp
+These will be of interest mainly to developers\&.
+.sp
+A \fI\-d\fR (debug) option before the main command word turns on debugging messages\&. Just one "\-d" gives you complete visibility about what commands the back end is running\&. It can be repeated for higher debugging levels that expose more of src\(cqs internal computation\&.
+.sp
+A \fI\-S\fR (sandbox) option can be used to set the repository directory to something other than its default of "\&.src"\&.
+.sp
+A \fI\-T\fR option sets up an artificial clock that ticks once on each revision and fixes the user information to be used in \fIfast\-export\fR; It also attributes all commits to "J\&. Random Hacker"\&. It is for regression testing\&.
+.SH "REQUIREMENTS"
+.sp
+SRC is written in Python and requires 2\&.7 or later; it will run under Python 3\&.x\&.
+.sp
+If you wish to use the RCS support, the RCS tools at version 5\&.7 or later must be installed and accessible in your path\&.
+.sp
+If you wish to use the SCCS support, the CSSC (Compatibly Stupid Source Control) system must be installed and accessible in your path\&.
+.sp
+The rcs\-fast\-import(1) tool at version 1\&.0 or later is required to support the \fIsrc fast\-import\fR command\&.
+.sp
+src will die gracefully with a useful error message when it fails due to a missing back end\&.
+.SH "LIMITATIONS"
+.sp
+Branch deletions change the revision numbers of revisions downstream of the branch join\&. This behavior may change in a future release\&.
+.sp
+The SCCS backend has a Y2\&.1K problem that SRC cannot effectively work around and will probably not be fixed\&.
+.SH "REPORTING BUGS"
+.sp
+Report bugs to Eric S\&. Raymond <esr@thyrsus\&.com>\&. The project page is at http://catb\&.org/~esr/src
+.SH "SEE ALSO"
+.sp
+rcs(1), rcs\-fast\-import(1), sccs(1), svn(1), git(1), dot(1)\&.
diff --git a/src.asc b/src.asc
new file mode 100644
index 0000000..2469883
--- /dev/null
+++ b/src.asc
@@ -0,0 +1,313 @@
+= src(1) =
+:doctype: manpage
+
+== NAME ==
+src - simple revision control
+
+== SYNOPSIS ==
+*src* [command] [revision-spec] ['file'...]
+
+== DESCRIPTION ==
+
+SRC (or src) is simple revision control, a version-control system for
+single-file projects by solo developers and authors. It modernizes the
+venerable RCS, hence the anagrammatic acronym. The design is tuned for
+use cases like all those little scripts in your "~/bin" directory, or a
+directory full of single-file HOWTOs.
+
+SRC revision histories are single, human-readable files beneath a hidden
+".src" subdirectory in the directory where they live. There may be
+multiple histories under one directory; SRC treats them as separate
+projects, and history files can be moved elsewhere at any time.
+
+SRC gives you simple, consecutive integer revision numbers. It supports
+tags and branching. It does not show committer information, because the
+committer is always you. The command set is intended to look familiar
+if you have ever used Subversion, Mercurial, or Git.
+
+SRC interprets the EDITOR variable in the usual way, using it to spawn
+an editor instance when you perform a commit or amend.
+
+SRC is fully supported in Emacs VC mode.
+
+== COMMAND SUMMARY ==
+
+A "revision" is a 1-origin integer, or a tag name designating an integer
+revision, or a branch name designating the tip revision of its branch.
+Revision numbers always increase in commit-date order.
+
+A revision range is a single revision, or a pair of revisions "M-N" (all
+revisions numerically from M to N) or "M..N" (all revisions that are
+branch ancestors of N and branch successors of M).
+
+If SRC complains that your revision spec looks like a nonexistent
+filename, you can prefix it with "@" (this is always allowed). "@" by
+itself means the current (checked-out) revision.
+
+Unless otherwise noted under individual commands, the default revision
+is the tip revision on the current branch and the default range is all
+revisions on the current branch.
+
+The token "--" tells the command-line interpreter that subcommands,
+switches, and revision-specs are done - everything after it is a
+filename, even if it looks like a subcommand or revision number.
+
+src help ['command']::
+ Displays help for commands.
+
+src commit [-|-m 'string'|-f 'file'|-e] ['file'...]::
+ Enters a commit for specified files. Separately to each one. A
+ history is created for the file if it does not already exist. With
+ '-', take comment text from stdin; with '-m' use the following
+ string as the comment; with '-f' take from a file. With '-e', edit
+ even after '-', '-f' or '-m'. "ci" is a synonym for "commit".
+
+src amend [-|-m 'string'|-f 'file'|-e] ['revision'] ['file'...]::
+ Amends the stored comment for a specified revision, defaulting to
+ the latest revision on the current branch. Flags are as for commit.
+ "am" is a synonym for "amend".
+
+src checkout ['revision'] ['file'...]::
+ Refresh the working copy of the file(s) from their history files.
+ "co" is a synonym for "checkout".
+
+src cat ['revision'] ['file'...]::
+ Send the specified revision of the files to standard output.
+
+src status [-a] ['file'...]::
+ "A" = added, "=" = unmodified, "M" = modified, "!" = missing, "?"
+ = not tracked, "I" = ignored, "L" = locked (recover with "src
+ checkout"). The "A" and "L" statuses should only occur if you have
+ used RCS directly on a file. Normally "?" and "I" files are not
+ listed; this changes if you either give the '-a' switch or specify
+ which files to look at. "st" is a synonym for "status".
+
+src tag [list|-l|create|-c|delete|del|-d] ['name'] ['revision'] ['file'...]::
+ List tags, create tags, or delete tags. Create/delete takes a
+ revision, defaulting to the current branch tip. List to all
+ revisions.
+
+src branch [list|-l|create|-c|delete|del|-d] ['name'] ['file'...]::
+ List, create, or delete branches. When listing, the active branch
+ is first in the list. The default branch is "trunk". Create/delete
+ takes a singleton revision, defaulting to the current branch tip.
+ List defaults to all revisions, including 0 (the trunk root phantom
+ revision).
+
+src rename ['tag'|'branch'] ['oldname'] ['newname'] ['file'...]::
+ Rename a tag or branch. Refuses to step on an existing symbol or
+ rename a nonexistent one. "rn" is a synonym for "rename".
+
+src list [(-<n>|-l <n>)] [-f 'fmt'] ['revision-range'] ['file'...]::
+ Sends summary information about the specified commits to standard
+ output. The summary line tagged with "*" is the state that the
+ file would return to on checkout without a revision-spec. See 'help
+ list' for information about custom formats. Use '-<n>' or '-l <n>',
+ where <n> is a number, to limit the listing length. Default range
+ is the current branch, reversed. "li" is a synonym for "list".
+
+src log [-v] [(-<n>|-l <n>)] [(-p|-u|-c) [-b|-w]] ['revision-range'] ['file'...]::
+ Sends log information about the specified commits to standard
+ output. Use '-<n>' or '-l <n>', where <n> is a number, to limit the
+ listing length. Default range is the current branch, reversed.
+ The '--patch', '-p' or '-u' option additionally sends a
+ unified format diff listing to standard output for each revision
+ against its immediate ancestor revision; '-c' emits a context diff
+ instead. When generating a diff, '-b' ignores changes in whitespace,
+ and '-w' ignores all whitespace. Histories imported via
+ 'fast-import' (when not using its '-p' option) have RFC-822-style
+ headers inserted into the log comment to preserve metadata not
+ otherwise representable in SRC, such as distinct author and
+ committer identifications and dates. These headers are normally
+ suppressed by 'log', however, '-v' shows a summarized view of
+ important headers; '-v -v' shows all headers as-is.
+
+src diff [(-u|-c) [-b|-w]] ['revision-range'] ['file'...]::
+ Sends a diff listing to standard output. With no revision spec,
+ diffs the working copy against the last version checked in. With
+ one revno, diffs the working copy against that stored revision; with
+ a range, diff between the beginning and end of the range.
+ The actual difference generation is done with diff(1). The default
+ diff format is '-u' (unified), but if you specify a '-c' option
+ after the verb a context diff will be emitted. '-b' ignores changes
+ in the amount of whitespace, and '-w' ignores all whitespace.
+ "di" is a synonym for "diff".
+
+src ls::
+ List all registered files.
+
+src visualize::
+ Emit a DOT visualization of repository structures. To use this,
+ install the graphviz package and pipe the output to something like
+ "dot -Tpng | display -". "vis" is a synonym for "visualize".
+
+src move 'old' 'new'::
+ Rename a workfile and its history. Refuses to step on existing
+ workfiles or histories. "mv" is a synonym for "move".
+
+src copy 'old' 'new'::
+ Copy a file and its master. Refuses to step on existing files or
+ masters. "cp" is a synonym for "copy".
+
+src fast-export ['revision-range'] ['file'...]::
+ Export one or more projects as a Git fast-import stream. For a
+ history originally imported from elsewhere, author and committer
+ identification is gleaned from the RFC-822-style headers inserted
+ into the commit comment by 'fast-import' (if its '-p' option was not
+ used). Otherwise, this information is copied from your Git
+ configuration.
+
+src fast-import [-p] ['files'...]::
+ Parse a git-fast-import stream from standard input. The
+ modifications for each individual file become separate
+ SRC histories. Mark, committer and author data, and mark
+ cross-references to parent commits, are preserved in RFC-822-style
+ headers on log comments unless the '-p' (plain) option is given, in
+ which case this metadata is discarded. Give arguments to restrict
+ the files imported.
+
+src release ['file'...]::
+ Release locks on files. This is never necessary in a normal
+ workflow, which will be repeated edit-commit cycles, but it may be
+ handy if you have to interoperate with other tools that expect RCS
+ masters to be in their normal (unlocked) state.
+
+src version::
+ Report the versions of SRC, the underlying Python, and the back end.
+
+The omission of "src remove" is a deliberate speed bump.
+
+If no files are specified, all eligible files are operated on in
+sequence.
+
+== NOISE CONTROL ==
+
+Silence is golden. When you have selected only one file to operate
+on, and the command is not a report generator ('status', 'cat', 'log',
+'list', 'fast-export', the listing modes of 'tag' and 'branch', 'ls')
+you will see a reply only if the operation failed.
+
+Other commands ('commit', 'checkout', tag creation and deletion) give
+you a success message per file when operating on multiple files, so you
+will not be in doubt about which operation succeeded. This behavior can
+be suppressed with the '-q' option, placed 'before' the subcommand word.
+
+If your directory contains a file named ".srcignore", each line that is
+neither blank nor begins with a "#" is interpreted as an ignore pattern.
+It is expanded with glob(3), and files in the expansion are omitted
+from 'src status' - unless the file is named as an argument, of the
+status command, in which case its status is "I". Thus, for example, a
+line reading "*.html" will cause all files with an HTML extension to be
+omitted from the output of "src status", but the output of 'src status
+*' will list them with status "I".
+
+== BACKWARD COMPATIBILITY ==
+
+SRC history files are (normally) RCS master files. SRC maintains no
+permanent state other than these files.
+
+SRC takes over the little-used "description" field of RCS (and SCCS)
+master files to store some state that makes status checks faster. If
+you try to use SRC to edit a pre-existing RCS- or SCCS-registered file
+with a non-empty description field, SRC will refuse to step on the old
+description; you must clear it manually.
+
+In order to maintain complete backwards compatibility, one other
+compromise was made: any commit comment containing a string exactly
+matching an RCS log delimiter (a long string of "-" characters) will be
+rejected as malformed.
+
+The RCS back end will be automatically selected when there is an ".src"
+or "RCS" subdirectory.
+
+You can explicitly select the RCS back end by making the first command
+keyword on the src command line be 'rcs'. This should only be necessary
+when your working directory contains two or more of the subdirectories
+".src", "RCS", and "SCCS".
+
+By default, history files are kept in a hidden subdirectory named
+".src". But if you have an RCS subdirectory and no ".src", SRC will
+quietly operate on the files in the RCS directory in a completely
+backward-compatible way.
+
+== WORKING WITH SCCS ==
+
+Using SCCS as a back end is also supported, with some limits due to
+missing features in SCCS:
+
+* all commands relating to tags and branches throw an error.
+
+* src cat does not necessarily pipe out binary data correctly.
+
+* the exec bit is not propagated between master and workfile.
+
+The SCCS back end will be automatically selected when there is an "SCCS"
+subdirectory and no ".src" or "RCS" subdirectory.
+
+You can explicitly select the SCCS back end by making the first command
+keyword on the src command line be 'sccs'. This should only be
+necessary when your working directory contains none or two or more of
+the subdirectories ".src", "RCS", and "SCCS".
+
+Working with SCCS requires an "SCCS" subdirectory; SRC will quietly
+create one, if required, then operate on the files in the "SCCS"
+directory in a completely backward-compatible way.
+
+Fast-import to SCCS is not supported.
+
+The SCCS mode is not recommended unless you have a specific need to
+work with legacy SCCS repositories. Up-converting them to something
+less Paleolithic would be a better idea; in truth, the SCCS support
+exists mainly because it's hilarious (for obscure hacker values of
+'hilarious').
+
+== DEBUGGING OPTIONS ==
+
+These will be of interest mainly to developers.
+
+A '-d' (debug) option before the main command word turns on debugging
+messages. Just one "-d" gives you complete visibility about what
+commands the back end is running. It can be repeated for higher
+debugging levels that expose more of src's internal computation.
+
+A '-S' (sandbox) option can be used to set the repository directory to
+something other than its default of ".src".
+
+A '-T' option sets up an artificial clock that ticks once on each
+revision and fixes the user information to be used in 'fast-export'; It
+also attributes all commits to "J. Random Hacker". It is for regression
+testing.
+
+== REQUIREMENTS ==
+
+SRC is written in Python and requires 2.7 or later; it will run under
+Python 3.x.
+
+If you wish to use the RCS support, the RCS tools at version 5.7 or
+later must be installed and accessible in your path.
+
+If you wish to use the SCCS support, the CSSC (Compatibly Stupid Source
+Control) system must be installed and accessible in your path.
+
+The rcs-fast-import(1) tool at version 1.0 or later is required to
+support the 'src fast-import' command.
+
+src will die gracefully with a useful error message when it fails
+due to a missing back end.
+
+== LIMITATIONS ==
+
+Branch deletions change the revision numbers of revisions downstream of
+the branch join. This behavior may change in a future release.
+
+The SCCS backend has a Y2.1K problem that SRC cannot effectively work
+around and will probably not be fixed.
+
+== REPORTING BUGS ==
+
+Report bugs to Eric S. Raymond <esr@thyrsus.com>. The project page is
+at http://catb.org/~esr/src
+
+== SEE ALSO ==
+
+rcs(1), rcs-fast-import(1), sccs(1), svn(1), git(1), dot(1).
diff --git a/srctest b/srctest
new file mode 100755
index 0000000..de0b94f
--- /dev/null
+++ b/srctest
@@ -0,0 +1,1512 @@
+#!/bin/sh
+#
+# Regression tester for SRC.
+#
+
+# Set the umask for a bit of defensiveness
+umask 0077
+
+# Needs to not be a subdirectory of here, or git gets confused. Use
+# -u so that src's initialization will be tested.
+SANDBOX=$(mktemp -u /tmp/src_XXXXXXXX)
+
+# Set the PATH to include the current directory, so the repository
+# head version of src can always be tested.
+PATH="$(pwd)":$PATH
+
+trap "rm -fr $SANDBOX" 0 1 2 15
+
+backend="rcs"
+python="python"
+
+while getopts b:p:e: opt
+do
+ case $opt in
+ b) backend=$OPTARG;;
+ p) python=$OPTARG;;
+ e) src=$OPTARG;;
+ esac
+done
+shift $(($OPTIND - 1))
+
+$backend >/dev/null 2>&1
+if [ "$?" = 127 ]
+then
+ echo "srctest: backend ${backend} is missing."
+ exit 1
+fi
+
+if [ -z "$src" ]
+then
+ src="${python} ${PWD}/src"
+fi
+
+check() {
+ case $? in
+ 0) echo "srctest ($python $backend): $1 succeeded";;
+ *) echo "srctest ($python $backend): $1 failed"; exit 1;;
+ esac
+}
+
+historify () {
+ case $backend in
+ rcs) history=.src/$1,v ;;
+ sccs) history=SCCS/s.$1 ;;
+ esac
+}
+
+
+mkdir $SANDBOX
+check "scratch directory creation"
+
+cd $SANDBOX
+
+TESTOPTS="-T $* $backend"
+
+DIFFOPTS="--label Expected --label Actual -u"
+
+
+COLUMNS=73
+export COLUMNS
+
+cat >testfile1 <<EOF
+Now is the time
+EOF
+cp testfile1 testrev1
+
+$src $TESTOPTS commit -m "First comment" testfile1 #>/dev/null
+check "commit with -m option"
+
+$src $TESTOPTS cat testfile1 | diff -u testfile1 - >/dev/null
+check "content check after first commit"
+
+cat >testfile1 <<EOF
+Now is the time
+for all good men
+EOF
+cp testfile1 testrev2
+
+cat >testfile4 <<EOF
+@@ -1,2 +1,3 @@
+ Now is the time
++for all good men
+
+EOF
+
+# (read; read; cat) removes header lines containing variable date information
+$src $TESTOPTS diff testfile1 | (read discard; read discard; cat) >testfile5
+diff -u testfile4 testfile5
+check "nonempty diff"
+
+! $src $TESTOPTS diff -@ testfile1 2>diff.err &&
+ grep unexpected diff.err >/dev/null
+check "bogus diff option"
+
+sleep 1 # Force commit to have different timestamp
+echo "Second comment" | $src $TESTOPTS commit - testfile1 >/dev/null
+check "commit with -"
+
+$src $TESTOPTS cat testfile1 | diff $DIFFOPTS testfile1 -
+check "content check after second commit"
+
+cat >testfile2 <<EOF
+= testfile1 ============================================================
+2 * 1970-01-02T00:01:00Z Second comment
+1 - 1970-01-02T00:00:00Z First comment
+EOF
+
+$src $TESTOPTS list testfile1 >testfile3
+diff $DIFFOPTS testfile2 testfile3
+check "list test"
+
+cat >testfile2 <<EOF
+= testfile1 ============================================================
+2 | 1970-01-02T00:01:00Z | trunk
+Second comment
+------------------------------------------------------------------------
+1 | 1970-01-02T00:00:00Z | trunk
+First comment
+------------------------------------------------------------------------
+EOF
+
+$src $TESTOPTS log testfile1 >testfile3
+diff $DIFFOPTS testfile2 testfile3
+check "2-commit log test"
+
+cat >testfile1 <<EOF
+Now is the time
+for all good men
+to come to the aid of foo
+EOF
+cp testfile1 testrev3
+
+sleep 1 # Force commit to have different timestamp
+cat >testfile4 <<EOF
+'Third' comment
+
+Multiline test.
+
+EOF
+$src $TESTOPTS commit -f testfile4 testfile1 >/dev/null
+check "commit with -f"
+
+cat >testfile4 <<EOF
+= testfile1 ============================================================
+3 | 1970-01-02T00:02:00Z | trunk
+'Third' comment
+
+Multiline test.
+------------------------------------------------------------------------
+2 | 1970-01-02T00:01:00Z | trunk
+Second comment
+------------------------------------------------------------------------
+1 | 1970-01-02T00:00:00Z | trunk
+First comment
+------------------------------------------------------------------------
+EOF
+
+$src $TESTOPTS log testfile1 >testfile5
+diff $DIFFOPTS testfile4 testfile5
+check "3-commit log test"
+
+cat >testfile14 <<EOF
+= testfile1 ============================================================
+3 | 1970-01-02T00:02:00Z | trunk
+'Third' comment
+
+Multiline test.
+
+diff r2/testfile1 r3/testfile1
+--- testfile1 (r2)
++++ testfile1 (r3)
+@@ -1,3 +1,4 @@
+ Now is the time
+ for all good men
++to come to the aid of foo
+
+------------------------------------------------------------------------
+2 | 1970-01-02T00:01:00Z | trunk
+Second comment
+
+diff r1/testfile1 r2/testfile1
+--- testfile1 (r1)
++++ testfile1 (r2)
+@@ -1,2 +1,3 @@
+ Now is the time
++for all good men
+
+------------------------------------------------------------------------
+1 | 1970-01-02T00:00:00Z | trunk
+First comment
+
+diff /dev/null r1/testfile1
+--- /dev/null
++++ testfile1 (r1)
+@@ -1 +1,2 @@
++Now is the time
+
+------------------------------------------------------------------------
+EOF
+
+$src $TESTOPTS log --patch testfile1 >testfile15
+diff $DIFFOPTS testfile14 testfile15
+check "log --patch"
+
+cat >testfile6 <<EOF
+= testfile1 ============================================================
+src: testfile1 has no 5 revision
+EOF
+
+$src $TESTOPTS list 5 testfile1 >testfile7 2>&1
+diff $DIFFOPTS testfile6 testfile7
+check "oob revision spec check"
+
+for rev in 1 2 3
+do
+ $src $TESTOPTS checkout $rev testfile1 >/dev/null
+ check "revision $rev checkout test"
+ diff $DIFFOPTS testrev${rev} testfile1
+ check "revision $rev content test"
+done
+
+if [ "$backend" = "sccs" ]
+then
+ echo "srctest ($python $backend): skipping tag creation/deletion/listing checks."
+else
+ $src $TESTOPTS tag create sampletag
+ check "tag set to default tip revision"
+
+ cat >testfile8 <<EOF
+= testfile1 ==========================================================
+ 3 sampletag
+EOF
+ $src $TESTOPTS tag list >testfile9
+ diff $DIFFOPTS testfile8 testfile9
+ check "tag list check"
+
+ $src $TESTOPTS tag create basetag 1
+ check "tag set to revision 1"
+
+ cat >testfile10 <<EOF
+= testfile1 ==========================================================
+ 1 basetag
+ 3 sampletag
+EOF
+ $src $TESTOPTS tag -l >testfile11
+ diff $DIFFOPTS testfile10 testfile11
+ check "second tag list check"
+
+ $src $TESTOPTS tag delete sampletag
+ check "tag deletion: sampletag"
+
+ cat >testfile12 <<EOF
+= testfile1 ==========================================================
+ 1 basetag
+EOF
+ $src $TESTOPTS tag >testfile13
+ diff $DIFFOPTS testfile12 testfile13
+ check "tag list check after deletion"
+
+ # Alas, we have to do this or the fast-export regression will fail.
+ # We don't know how to be perfectly canonical about tags yet.
+ $src $TESTOPTS tag delete basetag
+ check "tag deletion: basetag"
+fi
+
+test_export () {
+ testname="$1"
+ shift
+ srcfi=$testname-src.fi
+ gitfi=$testname-git.fi
+ mkdir foo
+ $src $TESTOPTS fast-export "$@" >$srcfi 2>export.err
+ grep jrh $srcfi >/dev/null
+ check "fast-export: $testname"
+ cat $srcfi | (cd foo >/dev/null; git init --quiet; git fast-import --quiet)
+ if ! grep refs/heads/master $srcfi >/dev/null
+ then
+ perl -0777 -pi -e 's|^reset refs/heads/.+?\nfrom :.+?\n\n||gm' $srcfi
+ fi
+ (cd foo >/dev/null; git fast-export --all) >$gitfi
+ diff $DIFFOPTS $srcfi $gitfi
+ check "fast-export roundtrip: $testname"
+ rm -fr foo
+}
+
+test_export filename testfile1
+
+echo flower power >testfile14
+$src $TESTOPTS commit -m "Alfred E. Newman" testfile14 >/dev/null
+test_export filenames testfile1 testfile14
+historify testfile14
+rm -f $history
+
+test_export revspec-filename -- testfile1
+grep refs/heads/master revspec-filename-src.fi >/dev/null &&
+ ! grep refs/heads/testfile1/master revspec-filename-src.fi >/dev/null
+check "fast-export revspec/filename distinction"
+
+rm -f testfile1
+test_export not-checked-out testfile1
+check "fast-export consults history only"
+$src $TESTOPTS checkout testfile1 >/dev/null
+
+if [ "$backend" = "sccs" ]
+then
+ echo "srctest ($python $backend): skipping fast-import checks: RCS-only"
+elif ! command -v rcs-fast-import >/dev/null 2>&1
+then
+ echo "srctest ($python $backend): skipping fast-import checks: rcs-fast-import missing"
+else
+ mkdir RCS
+ ! $src $TESTOPTS fast-import -p <filename-git.fi >/dev/null 2>err &&
+ grep 'existing RCS' err >/dev/null
+ check "fast-import don't clobber RCS"
+ rm -fr RCS
+
+ historify testfile1
+ rm -f $history
+ $src $TESTOPTS fast-import -p <filename-git.fi >/dev/null 2>&1 &&
+ test -f $history
+ check "fast-import"
+
+ $src $TESTOPTS fast-export testfile1 >testfile1.fi &&
+ diff -u filename-git.fi testfile1.fi
+ check "fast-import roundtrip"
+fi
+
+$src $TESTOPTS move testfile1 newname1
+check "move command"
+historify newname1
+if [ -e newname1 -a -e $history ]
+then
+ echo "srctest ($python $backend): content move succeeded"
+else
+ echo "srctest ($python $backend): content move failed"
+ exit 1
+fi
+
+$src $TESTOPTS copy newname1 newname2
+check "copy command"
+historify newname2
+if [ -e newname2 -a -e $history ]
+then
+ echo "srctest ($python $backend): content copy succeeded"
+else
+ echo "srctest ($python $backend): content copy failed"
+ exit 1
+fi
+
+cat >testfile16 <<EOF
+newname1
+newname2
+EOF
+$src $TESTOPTS ls | diff $DIFFOPTS testfile16 -
+check "ls"
+
+$src $TESTOPTS commit -m "Another sample" testfile16
+$src $TESTOPTS status testfile16 | grep "^=" >/dev/null
+check "= status check after first commit"
+
+echo "Add a second line" >>testfile16
+$src $TESTOPTS status testfile16 | grep "^M" >/dev/null
+check "M status check after modification"
+
+rm testfile16
+$src $TESTOPTS status testfile16 | grep "^!" >/dev/null
+check "! status check after deletion"
+
+$src $TESTOPTS checkout testfile16
+$src $TESTOPTS status testfile16 | grep "^=" >/dev/null
+check "= status check after restoration"
+
+historify testfile16
+rm -f $history
+$src $TESTOPTS status testfile16 | grep "^?" >/dev/null
+check "? status check after master deletion"
+
+$src $TESTOPTS amend -m "Amended comment" newname1
+check "amend"
+
+$src $TESTOPTS list 3 newname1 | grep "Amended comment" >/dev/null
+check "amended comment content"
+
+cat >testfile17 <<EOF
+= newname1
+= newname2
+EOF
+$src $TESTOPTS status | diff $DIFFOPTS testfile17 - >/dev/null
+check "unadorned status command"
+
+if [ "$backend" = "sccs" ]
+then
+ echo "srctest ($python $backend): skipping tag and branch tests"
+else
+ # Introduce a test history with some branchiness, so
+ # we can test traversal across branch joins. Also has tags.
+ cat >.src/sample,v <<EOF
+head 1.4;
+branch 1.3.1.4.1;
+access;
+symbols
+ GLARB:1.4
+ GORP:1.3.1.4
+ sample:1.3.1.4.0.1
+ muggle:1.3.0.1;
+locks
+ esr:1.3.1.4.1.5;
+comment @# @;
+expand @b@;
+
+
+1.4
+date 2014.11.11.03.26.53; author esr; state Exp;
+branches;
+next 1.3;
+
+1.3
+date 2014.11.10.23.48.49; author esr; state Exp;
+branches
+ 1.3.1.1;
+next 1.2;
+
+1.2
+date 2014.11.10.23.48.11; author esr; state Exp;
+branches;
+next 1.1;
+
+1.1
+date 2014.11.10.23.47.08; author esr; state Exp;
+branches;
+next ;
+
+1.3.1.1
+date 2014.11.11.04.23.47; author esr; state Exp;
+branches;
+next 1.3.1.2;
+
+1.3.1.2
+date 2014.11.11.06.39.47; author esr; state Exp;
+branches;
+next 1.3.1.3;
+
+1.3.1.3
+date 2014.11.11.06.48.00; author esr; state Exp;
+branches;
+next 1.3.1.4;
+
+1.3.1.4
+date 2014.11.12.03.36.45; author esr; state Exp;
+branches
+ 1.3.1.4.1.1;
+next ;
+
+1.3.1.4.1.1
+date 2014.11.13.04.04.55; author esr; state Exp;
+branches;
+next 1.3.1.4.1.2;
+
+1.3.1.4.1.2
+date 2014.11.13.04.23.04; author esr; state Exp;
+branches;
+next 1.3.1.4.1.3;
+
+1.3.1.4.1.3
+date 2014.11.13.04.26.23; author esr; state Exp;
+branches;
+next 1.3.1.4.1.4;
+
+1.3.1.4.1.4
+date 2014.11.13.04.30.39; author esr; state Exp;
+branches;
+next 1.3.1.4.1.5;
+
+1.3.1.4.1.5
+date 2014.11.13.04.31.02; author esr; state Exp;
+branches;
+next ;
+
+
+desc
+@@
+
+
+1.4
+log
+@On a branch?
+@
+text
+@Now is the time
+For all good men.
+To come to the aid of foo.
+This should be branch text.
+@
+
+
+1.3
+log
+@Third comment.
+@
+text
+@d4 1
+@
+
+
+1.3.1.1
+log
+@Totally unfubared.
+@
+text
+@a3 2
+
+Again, this should be branch text.
+@
+
+
+1.3.1.2
+log
+@Still utterly fubar
+@
+text
+@d5 1
+a5 1
+Yet again, this should be branch text.
+@
+
+
+1.3.1.3
+log
+@Totally chenille.
+@
+text
+@a5 2
+
+Revision 7.
+@
+
+
+1.3.1.4
+log
+@Utterly fubar
+@
+text
+@d4 4
+a7 2
+This should be branch text.
+Try a comment with a hyphen.
+@
+
+
+1.3.1.4.1.1
+log
+@All fixed up.
+@
+text
+@a5 1
+Test of commit comments.
+@
+
+
+1.3.1.4.1.2
+log
+@It' all good.
+@
+text
+@a6 1
+Glotch.
+@
+
+
+1.3.1.4.1.3
+log
+@No good.
+@
+text
+@a7 1
+Random modification.
+@
+
+
+1.3.1.4.1.4
+log
+@That's good.
+@
+text
+@a4 1
+jjjj
+d7 1
+@
+
+
+1.3.1.4.1.5
+log
+@I see you
+@
+text
+@d5 1
+a5 1
+jjjjjjjjkkk
+@
+
+
+1.2
+log
+@Second comment.
+@
+text
+@d3 1
+@
+
+
+1.1
+log
+@First comment.
+@
+text
+@d2 1
+@
+EOF
+
+cat >sampledot <<EOF
+digraph {
+ 1 [shape=box,width=5,label=<<table cellspacing="0" border="0" cellborder="0"><tr><td><font color="blue">1</font></td><td>First comment.</td></tr></table>>];
+ 1 -> 2;
+ 2 [shape=box,width=5,label=<<table cellspacing="0" border="0" cellborder="0"><tr><td><font color="blue">2</font></td><td>Second comment.</td></tr></table>>];
+ 2 -> 3;
+ 3 [shape=box,width=5,label=<<table cellspacing="0" border="0" cellborder="0"><tr><td><font color="blue">3</font></td><td>Third comment.</td></tr></table>>];
+ 3 -> 4;
+ 4 [shape=box,width=5,label=<<table cellspacing="0" border="0" cellborder="0"><tr><td><font color="blue">4</font></td><td>On a branch?</td></tr></table>>];
+ "trunk" [shape=oval,width=2];
+ "4" -> "trunk" [style=dotted];
+ 3 -> 5;
+ 5 [shape=box,width=5,label=<<table cellspacing="0" border="0" cellborder="0"><tr><td><font color="blue">5</font></td><td>Totally unfubared.</td></tr></table>>];
+ 5 -> 6;
+ 6 [shape=box,width=5,label=<<table cellspacing="0" border="0" cellborder="0"><tr><td><font color="blue">6</font></td><td>Still utterly fubar</td></tr></table>>];
+ 6 -> 7;
+ 7 [shape=box,width=5,label=<<table cellspacing="0" border="0" cellborder="0"><tr><td><font color="blue">7</font></td><td>Totally chenille.</td></tr></table>>];
+ 7 -> 8;
+ 8 [shape=box,width=5,label=<<table cellspacing="0" border="0" cellborder="0"><tr><td><font color="blue">8</font></td><td>Utterly fubar</td></tr></table>>];
+ "muggle" [shape=oval,width=2];
+ "8" -> "muggle" [style=dotted];
+ 8 -> 9;
+ 9 [shape=box,width=5,label=<<table cellspacing="0" border="0" cellborder="0"><tr><td><font color="blue">9</font></td><td>All fixed up.</td></tr></table>>];
+ 9 -> 10;
+ 10 [shape=box,width=5,label=<<table cellspacing="0" border="0" cellborder="0"><tr><td><font color="blue">10</font></td><td>It' all good.</td></tr></table>>];
+ 10 -> 11;
+ 11 [shape=box,width=5,label=<<table cellspacing="0" border="0" cellborder="0"><tr><td><font color="blue">11</font></td><td>No good.</td></tr></table>>];
+ 11 -> 12;
+ 12 [shape=box,width=5,label=<<table cellspacing="0" border="0" cellborder="0"><tr><td><font color="blue">12</font></td><td>That's good.</td></tr></table>>];
+ 12 -> 13;
+ 13 [shape=box,width=5,label=<<table cellspacing="0" border="0" cellborder="0"><tr><td><font color="blue">13</font></td><td>I see you</td></tr></table>>];
+ "sample" [shape=oval,width=2];
+ "13" -> "sample" [style=dotted];
+ {rank=same; "GLARB"; "4"}
+ "GLARB" -> "4" [style=dotted];
+ {rank=same; "GORP"; "8"}
+ "GORP" -> "8" [style=dotted];
+}
+EOF
+
+ $src $TESTOPTS visualize sample >tryit
+ diff $DIFFOPTS sampledot tryit
+ check "dot visualization"
+
+ cat >statuslog <<EOF
+= newname1
+= newname2
+EOF
+
+ cat >sample.seqlog <<EOF
+= sample ===============================================================
+6 | 1970-01-02T00:05:00Z | muggle
+Still utterly fubar
+------------------------------------------------------------------------
+5 | 1970-01-02T00:04:00Z | muggle
+Totally unfubared.
+------------------------------------------------------------------------
+4 | 1970-01-02T00:03:00Z | trunk
+On a branch?
+------------------------------------------------------------------------
+3 | 1970-01-02T00:02:00Z | trunk
+Third comment.
+------------------------------------------------------------------------
+2 | 1970-01-02T00:01:00Z | trunk
+Second comment.
+------------------------------------------------------------------------
+EOF
+
+ $src $TESTOPTS log 6-2 sample >tryit
+ diff $DIFFOPTS sample.seqlog tryit
+ check "traversal by -"
+
+ cat >sample.branchlog <<EOF
+= sample ===============================================================
+6 | 1970-01-02T00:05:00Z | muggle
+Still utterly fubar
+------------------------------------------------------------------------
+5 | 1970-01-02T00:04:00Z | muggle
+Totally unfubared.
+------------------------------------------------------------------------
+3 | 1970-01-02T00:02:00Z | trunk
+Third comment.
+------------------------------------------------------------------------
+2 | 1970-01-02T00:01:00Z | trunk
+Second comment.
+------------------------------------------------------------------------
+EOF
+
+ $src $TESTOPTS log 6..2 sample >tryit
+ diff $DIFFOPTS sample.branchlog tryit
+ check "traversal by .."
+
+ cat >sample.branchlog-p <<EOF
+= sample ===============================================================
+5 | 1970-01-02T00:04:00Z | muggle
+Totally unfubared.
+
+diff r3/sample r5/sample
+--- sample (r3)
++++ sample (r5)
+@@ -2,3 +2,5 @@
+ For all good men.
+ To come to the aid of foo.
+
++Again, this should be branch text.
++
+------------------------------------------------------------------------
+EOF
+
+ $src $TESTOPTS log --patch 5 sample >tryit
+ diff $DIFFOPTS sample.branchlog-p tryit
+ check "branchy log --patch"
+
+ cat >sample.taglog <<EOF
+= sample ===============================================================
+4 - 1970-01-02T00:03:00Z On a branch?
+EOF
+
+ $src $TESTOPTS list @GLARB sample >tryit
+ diff $DIFFOPTS sample.taglog tryit
+ check "named-tag lookup with @"
+
+ $src $TESTOPTS rename tag GLARB GROTTY sample
+ check "tag renaming"
+
+ cat >changes <<EOF
+= sample =============================================================
+ 8 GORP
+ 4 GROTTY
+EOF
+
+ $src $TESTOPTS tag -l sample >tryit
+ diff $DIFFOPTS tryit changes
+ check "tag list after rename"
+
+ cat >branchlist <<EOF
+= sample =============================================================
+ 0 trunk
+ 3 muggle
+ 8 sample
+EOF
+
+ $src $TESTOPTS branch -l sample >tryit
+ check "branch list"
+
+ $src $TESTOPTS branch -l sample >tryit
+ diff $DIFFOPTS branchlist tryit
+ check "branch list content"
+
+ cat >branchlog <<EOF
+= sample ===============================================================
+4 - 1970-01-02T00:03:00Z On a branch?
+EOF
+
+ $src $TESTOPTS list @trunk sample >branchlog
+ check "branch name resolution"
+
+ $src $TESTOPTS list @trunk sample >tryit
+ diff $DIFFOPTS branchlog tryit
+ check "branch name resolved content"
+
+ cat >statuslog <<EOF
+= newname1
+= newname2
+EOF
+
+ $src $TESTOPTS status newname1 newname2 >tryit
+ diff $DIFFOPTS statuslog tryit
+ check "status before ignore"
+
+ echo "newname1" >.srcignore
+
+ cat >statuslog <<EOF
+I newname1
+= newname2
+EOF
+
+ $src $TESTOPTS status newname1 newname2 >tryit
+ diff $DIFFOPTS statuslog tryit
+ check "status after ignore"
+
+ $src $TESTOPTS branch delete trunk sample
+ check "branch delete"
+
+ cat >newlist <<EOF
+= sample =============================================================
+ 0 trunk
+ 3 muggle
+ 7 sample
+EOF
+
+ $src $TESTOPTS branch -l sample >tryit
+ diff $DIFFOPTS newlist tryit
+ check "branch list check after delete"
+
+ cat >newlog <<EOF
+= sample ===============================================================
+12 | 1970-01-02T00:11:00Z | sample
+I see you
+------------------------------------------------------------------------
+11 | 1970-01-02T00:10:00Z | sample
+That's good.
+------------------------------------------------------------------------
+10 | 1970-01-02T00:09:00Z | sample
+No good.
+------------------------------------------------------------------------
+9 | 1970-01-02T00:08:00Z | sample
+It' all good.
+------------------------------------------------------------------------
+8 | 1970-01-02T00:07:00Z | sample
+All fixed up.
+------------------------------------------------------------------------
+7 | 1970-01-02T00:06:00Z | muggle
+Utterly fubar
+------------------------------------------------------------------------
+6 | 1970-01-02T00:05:00Z | muggle
+Totally chenille.
+------------------------------------------------------------------------
+5 | 1970-01-02T00:04:00Z | muggle
+Still utterly fubar
+------------------------------------------------------------------------
+4 | 1970-01-02T00:03:00Z | muggle
+Totally unfubared.
+------------------------------------------------------------------------
+3 | 1970-01-02T00:02:00Z | trunk
+Third comment.
+------------------------------------------------------------------------
+2 | 1970-01-02T00:01:00Z | trunk
+Second comment.
+------------------------------------------------------------------------
+1 | 1970-01-02T00:00:00Z | trunk
+First comment.
+------------------------------------------------------------------------
+EOF
+
+ $src $TESTOPTS log sample >tryit
+ diff $DIFFOPTS newlog tryit
+ check "content after branch deletion"
+fi
+
+echo "Random content for numeric file" >23
+$src $TESTOPTS commit -m "I don't know why you say goodbye" -- 23 >/dev/null
+check "commit of file with numeric name"
+
+cat >randomcontent <<EOF
+Random content for numeric file
+EOF
+
+$src $TESTOPTS cat -- 23 >tryit
+diff $DIFFOPTS randomcontent tryit
+check "cat of file with numeric name"
+
+# Eric Sunshine writes:
+#
+# The test itself is working properly. The problem is that stock SCCS
+# does not preserve executable permission on files under its control, so
+# the test is correctly failing.
+#
+# The GNU version of SCCS does provide an 'x' flag[1] for compatibility
+# with SCO OpenServer which enables executable permission preservation.
+# However, I haven't convinced myself that it would make sense to put in
+# the work to add executable-preservation support to the SCCS backend
+# for these special cases (GNU CSSC and SCO OpenServer).
+#
+# [1]: https://www.gnu.org/software/cssc/manual/Flags.html#Flag
+#
+if [ "$backend" = sccs ]
+then
+ echo "srctest ($python $backend): skipping exec-bit propagation test"
+else
+ echo "Should not be executable" >exectest
+ $src $TESTOPTS commit -m "First comment on exectest" exectest >/dev/null
+ check "first commit of exectest"
+ chmod a+x exectest
+ echo "Should be executable" >exectest
+ $src $TESTOPTS commit -m "Second comment on exectest" exectest >/dev/null
+ check "second commit of exectest"
+ ls -l exectest | grep '^...x' >/dev/null
+ check "propagation of exec bit"
+fi
+
+cat >diffall.expect <<EOF
+--- newname1 (r3)
++++ newname1 (workfile)
+@@ -1,4 +1,5 @@
+ Now is the time
+ for all good men
+ to come to the aid of foo
++more stuff
+
+--- newname2 (r3)
++++ newname2 (workfile)
+@@ -1,4 +1,5 @@
+ Now is the time
+ for all good men
+ to come to the aid of foo
++and even more
+
+EOF
+
+echo "more stuff" >>newname1
+echo "and even more" >>newname2
+$src $TESTOPTS diff >diffall.actual &&
+ diff $DIFFOPTS diffall.expect diffall.actual &&
+ $src $TESTOPTS checkout newname1 newname2 >/dev/null
+check "default file list"
+
+cat >limit.list <<EOF
+= newname2 =============================================================
+3 * 1970-01-02T00:02:00Z 'Third' comment
+2 - 1970-01-02T00:01:00Z Second comment
+EOF
+cat >limit.log <<EOF
+= newname2 =============================================================
+3 | 1970-01-02T00:02:00Z | trunk
+'Third' comment
+
+Multiline test.
+------------------------------------------------------------------------
+2 | 1970-01-02T00:01:00Z | trunk
+Second comment
+------------------------------------------------------------------------
+EOF
+for i in list log
+do
+ for j in -2 '-l 2'
+ do
+ $src $TESTOPTS $i $j newname2 >limit.expect
+ diff limit.$i limit.expect
+ check "$i $j"
+ done
+done
+
+# Test for Tom Willemse's multi-commit bug.
+echo "test 1" > file1
+echo "test 2" > file2
+$src $TESTOPTS commit -m "Initial commit" file1 file2 >/dev/null
+check "multi-file registration"
+echo "test 1 line 2" >> file1
+echo "test 2 line 2" >> file2
+$src $TESTOPTS commit -m "Second commit" file1 file2 >/dev/null
+check "multi-file commit"
+
+# Test the edit logic
+cat >modify <<'EOF'
+echo "Different first line" >modified$$
+cat $1 >>modified$$
+mv modified$$ $1
+EOF
+chmod a+x modify
+echo "test 1 line 3" >> file1
+EDITOR="./modify" $src $TESTOPTS commit file1
+check "edit logic"
+
+cat >binary <<EOF
+This file contains an 0xD3 (Meta-S) after the colon:Ó
+EOF
+cat >binary.chk <<EOF
+This file contains an 0xD3 (Meta-S) after the colon:Ó
+Additional content line.
+EOF
+$src $TESTOPTS commit -m "Test file containing binary byte." binary
+echo "Additional content line." >>binary
+$src $TESTOPTS commit -m "Binary file after modification." binary
+cmp binary binary.chk
+check "binary content in checkouts and commits"
+
+if [ "$backend" = "sccs" ]
+then
+ echo "srctest ($python $backend): skipping binary cat test"
+else
+ $src $TESTOPTS cat binary >catted
+ cmp binary catted
+ check "binary content in cat"
+fi
+
+cat >newline <<EOF
+This file contains a DOS newline 0x0D after the colon:
+EOF
+cat >newline.chk <<EOF
+This file contains a DOS newline 0x0D after the colon:
+Additional content line.
+EOF
+$src $TESTOPTS commit -m "Test file containing DOS newline." newline
+echo "Additional content line." >>newline
+$src $TESTOPTS commit -m "DOS newline file after modification." newline
+cmp newline newline.chk
+check "newline preservation in checkouts and commits"
+
+if [ "$backend" = "sccs" ]
+then
+ echo "srctest ($python $backend): skipping newline cat test"
+else
+ $src $TESTOPTS cat newline >ncatted
+ cmp newline ncatted
+ check "newline content in cat"
+fi
+
+cat >padcomment <<'EOF'
+printf "\n\n\n\n" >modified$$
+cat $1 >>modified$$
+mv modified$$ $1
+EOF
+chmod a+x padcomment
+echo gommelgor >padfile
+$src $TESTOPTS commit -m pingle padfile
+for i in commit amend
+do
+ echo "more stuff" >>padfile
+ EDITOR="./padcomment" $src $TESTOPTS $i padfile >padout
+ grep cancelled padout >/dev/null
+ check "whitespace-only $i cancel"
+ $src $TESTOPTS checkout padfile
+done
+
+cat >clonemsg <<'EOF'
+echo "I think I'm a clone now" >modified$$
+cat $1 >>modified$$
+mv modified$$ $1
+cp $1 clonedmsg
+EOF
+chmod a+x clonemsg
+echo gommelgor >diffledore
+$src $TESTOPTS commit -m pingle diffledore
+for i in commit amend
+do
+ test $i = commit && echo "more stuff" >>diffledore
+ EDITOR="./clonemsg" $src $TESTOPTS $i diffledore >/dev/null
+ grep -F -e 'Changes to be committed' clonedmsg >/dev/null &&
+ grep -F -e '@@ ' clonedmsg >/dev/null &&
+ grep -F -e '+++ ' clonedmsg >/dev/null &&
+ grep -F -e '--- ' clonedmsg >/dev/null
+ check "$i diff in boilerplate"
+done
+
+cat >dontinvoke <<'EOF'
+echo "panic!" >>nochanges
+EOF
+chmod a+x dontinvoke
+echo "bingleby" >canttouchthis
+$src $TESTOPTS commit -m pingle canttouchthis
+EDITOR="./dontinvoke" $src $TESTOPTS commit canttouchthis >>nochanges
+grep 'no changes to commit' nochanges >/dev/null &&
+ ! grep panic nochanges >/dev/null
+check "nothing to commit"
+
+cat >ignore.ws <<EOF
+Mary had a little lamb,
+whose fleece was white as snow.
+Everywhere that Mary went,
+the lamb was sure to go.
+EOF
+$src $TESTOPTS commit -m initial ignore.ws
+cat >ignore.ws <<EOF
+Gary had a little lamb,
+whose fleece was white as snow.
+Everywhere that Gary went,
+ the lamb was sure to go.
+EOF
+sleep 1 # Force commit to have different timestamp
+$src $TESTOPTS commit -m 's/Mary/Gary/ & whitespace' ignore.ws
+cat >ignore.expect-b <<EOF
+--- ignore.ws (r1)
++++ ignore.ws (r2)
+@@ -1,4 +1,4 @@
+-Mary had a little lamb,
++Gary had a little lamb,
+ whose fleece was white as snow.
+-Everywhere that Mary went,
+-the lamb was sure to go.
++Everywhere that Gary went,
++ the lamb was sure to go.
+
+EOF
+cat >ignore.expect-w <<EOF
+--- ignore.ws (r1)
++++ ignore.ws (r2)
+@@ -1,4 +1,4 @@
+-Mary had a little lamb,
++Gary had a little lamb,
+ whose fleece was white as snow.
+-Everywhere that Mary went,
++Everywhere that Gary went,
+ the lamb was sure to go.
+
+EOF
+for i in -b -w
+do
+ $src $TESTOPTS diff -u $i 1-2 ignore.ws >ignore.actual$i
+ diff ignore.expect$i ignore.actual$i
+ check "diff $i"
+done
+
+if [ "$backend" = "sccs" ]
+then
+ echo "srctest ($python $backend): skipping commit date tie-breaking tests"
+else
+ # Introduce a test history with all commits having same date so we can
+ # test native revision ID tie-breaking.
+ cat >.src/tiebreak,v <<EOF
+head 1.11;
+access;
+symbols
+ refs/heads/master:1.11;
+locks
+ sunshine:1.11; strict;
+comment @# @;
+
+
+1.11
+date 2017.11.21.06.45.15; author sunshine; state Exp;
+branches;
+next 1.10;
+
+1.10
+date 2017.11.21.06.45.15; author sunshine; state Exp;
+branches;
+next 1.9;
+
+1.9
+date 2017.11.21.06.45.15; author sunshine; state Exp;
+branches;
+next 1.8;
+
+1.8
+date 2017.11.21.06.45.15; author sunshine; state Exp;
+branches;
+next 1.7;
+
+1.7
+date 2017.11.21.06.45.15; author sunshine; state Exp;
+branches;
+next 1.6;
+
+1.6
+date 2017.11.21.06.45.15; author sunshine; state Exp;
+branches;
+next 1.5;
+
+1.5
+date 2017.11.21.06.45.15; author sunshine; state Exp;
+branches;
+next 1.4;
+
+1.4
+date 2017.11.21.06.45.15; author sunshine; state Exp;
+branches;
+next 1.3;
+
+1.3
+date 2017.11.21.06.45.15; author sunshine; state Exp;
+branches;
+next 1.2;
+
+1.2
+date 2017.11.21.06.45.15; author sunshine; state Exp;
+branches;
+next 1.1;
+
+1.1
+date 2017.11.21.06.45.15; author sunshine; state Exp;
+branches;
+next ;
+
+
+desc
+@@
+
+
+1.11
+log
+@Author: Eric Sunshine <sunshine@@sunshineco.com>
+Author-Date: Mon 20 Nov 2017 21:37:42 -0500
+Committer: Roy G. Biv <spectrum@@color.com>
+Committer-Date: Tue 21 Nov 2017 01:45:15 +0000
+Mark: :22
+Parents: :20
+
+eleventh
+@
+text
+@eleventh
+@
+
+
+1.10
+log
+@Author: Eric Sunshine <sunshine@@sunshineco.com>
+Author-Date: Mon 20 Nov 2017 21:30:40 -0500
+Committer: Roy G. Biv <spectrum@@color.com>
+Committer-Date: Tue 21 Nov 2017 01:45:15 +0000
+Mark: :20
+Parents: :18
+
+tenth
+@
+text
+@d1 1
+a1 1
+tenth
+@
+
+
+1.9
+log
+@Author: Eric Sunshine <sunshine@@sunshineco.com>
+Author-Date: Mon 20 Nov 2017 21:27:29 -0500
+Committer: Roy G. Biv <spectrum@@color.com>
+Committer-Date: Tue 21 Nov 2017 01:45:15 +0000
+Mark: :18
+Parents: :16
+
+ninth
+@
+text
+@d1 1
+a1 1
+ninth
+@
+
+
+1.8
+log
+@Author: Eric Sunshine <sunshine@@sunshineco.com>
+Author-Date: Mon 20 Nov 2017 21:25:22 -0500
+Committer: Roy G. Biv <spectrum@@color.com>
+Committer-Date: Tue 21 Nov 2017 01:45:15 +0000
+Mark: :16
+Parents: :14
+
+eighth
+@
+text
+@d1 1
+a1 1
+eighth
+@
+
+
+1.7
+log
+@Author: Eric Sunshine <sunshine@@sunshineco.com>
+Author-Date: Mon 20 Nov 2017 23:02:46 -0500
+Committer: Roy G. Biv <spectrum@@color.com>
+Committer-Date: Tue 21 Nov 2017 01:45:15 +0000
+Mark: :14
+Parents: :12
+
+seventh
+@
+text
+@d1 1
+a1 1
+seventh
+@
+
+
+1.6
+log
+@Author: Eric Sunshine <sunshine@@sunshineco.com>
+Author-Date: Mon 20 Nov 2017 22:56:46 -0500
+Committer: Roy G. Biv <spectrum@@color.com>
+Committer-Date: Tue 21 Nov 2017 01:45:15 +0000
+Mark: :12
+Parents: :10
+
+sixth
+@
+text
+@d1 1
+a1 1
+sixth
+@
+
+
+1.5
+log
+@Author: Eric Sunshine <sunshine@@sunshineco.com>
+Author-Date: Fri 03 Nov 2017 13:30:17 -0500
+Committer: Roy G. Biv <spectrum@@color.com>
+Committer-Date: Tue 21 Nov 2017 01:45:15 +0000
+Mark: :10
+Parents: :8
+
+fifth
+@
+text
+@d1 1
+a1 1
+fifth
+@
+
+
+1.4
+log
+@Author: Eric Sunshine <sunshine@@sunshineco.com>
+Author-Date: Fri 03 Nov 2017 13:28:35 -0500
+Committer: Roy G. Biv <spectrum@@color.com>
+Committer-Date: Tue 21 Nov 2017 01:45:15 +0000
+Mark: :8
+Parents: :6
+
+fourth
+@
+text
+@d1 1
+a1 1
+fourth
+@
+
+
+1.3
+log
+@Author: Eric Sunshine <sunshine@@sunshineco.com>
+Author-Date: Fri 03 Nov 2017 13:26:41 -0500
+Committer: Roy G. Biv <spectrum@@color.com>
+Committer-Date: Tue 21 Nov 2017 01:45:15 +0000
+Mark: :6
+Parents: :4
+
+third
+@
+text
+@d1 1
+a1 1
+third
+@
+
+
+1.2
+log
+@Author: Eric Sunshine <sunshine@@sunshineco.com>
+Author-Date: Fri 03 Nov 2017 13:25:01 -0500
+Committer: Roy G. Biv <spectrum@@color.com>
+Committer-Date: Tue 21 Nov 2017 01:45:15 +0000
+Mark: :4
+Parents: :2
+
+second
+@
+text
+@d1 1
+a1 1
+second
+@
+
+
+1.1
+log
+@Author: Eric Sunshine <sunshine@@sunshineco.com>
+Author-Date: Fri 03 Nov 2017 13:12:48 -0500
+Committer: Roy G. Biv <spectrum@@color.com>
+Committer-Date: Tue 21 Nov 2017 01:45:15 +0000
+Mark: :2
+
+first
+@
+text
+@d1 1
+a1 1
+first
+@
+EOF
+
+ cat >tiebreak.expect <<EOF
+= tiebreak =============================================================
+11 * 1970-01-02T00:10:00Z eleventh
+10 - 1970-01-02T00:09:00Z tenth
+9 - 1970-01-02T00:08:00Z ninth
+8 - 1970-01-02T00:07:00Z eighth
+7 - 1970-01-02T00:06:00Z seventh
+6 - 1970-01-02T00:05:00Z sixth
+5 - 1970-01-02T00:04:00Z fifth
+4 - 1970-01-02T00:03:00Z fourth
+3 - 1970-01-02T00:02:00Z third
+2 - 1970-01-02T00:01:00Z second
+1 - 1970-01-02T00:00:00Z first
+EOF
+
+ $src $TESTOPTS list tiebreak >tiebreak.actual
+ diff tiebreak.expect tiebreak.actual
+ check "same date tie-breaking"
+
+ cat >noheaders.expect <<EOF
+= tiebreak =============================================================
+11 | 1970-01-02T00:10:00Z | trunk
+eleventh
+------------------------------------------------------------------------
+10 | 1970-01-02T00:09:00Z | trunk
+tenth
+------------------------------------------------------------------------
+EOF
+
+ $src $TESTOPTS log -2 tiebreak >noheaders.actual
+ diff noheaders.expect noheaders.actual
+ check "log suppresses headers"
+
+ cat >logheaders.expect <<EOF
+= tiebreak =============================================================
+11 | 1970-01-02T00:10:00Z | trunk
+Author: Eric Sunshine <sunshine@sunshineco.com>
+Author-Date: 2017-11-21T02:37:42Z
+Author-Date-Offset: -18000
+Committer: Roy G. Biv <spectrum@color.com>
+Committer-Date: 2017-11-21T01:45:15Z
+Committer-Date-Offset: 0
+Mark: :22
+Parents: :20
+
+eleventh
+------------------------------------------------------------------------
+10 | 1970-01-02T00:09:00Z | trunk
+Author: Eric Sunshine <sunshine@sunshineco.com>
+Author-Date: 2017-11-21T02:30:40Z
+Author-Date-Offset: -18000
+Committer: Roy G. Biv <spectrum@color.com>
+Committer-Date: 2017-11-21T01:45:15Z
+Committer-Date-Offset: 0
+Mark: :20
+Parents: :18
+
+tenth
+------------------------------------------------------------------------
+EOF
+
+ $src $TESTOPTS log -v -v -2 tiebreak >logheaders.actual
+ diff logheaders.expect logheaders.actual
+ check "log -v -v shows all headers"
+
+ cat >summaryheaders.expect <<EOF
+= tiebreak =============================================================
+11 | 1970-01-02T00:10:00Z | trunk
+Author: Eric Sunshine <sunshine@sunshineco.com> 2017-11-21T02:37:42Z
+Committer: Roy G. Biv <spectrum@color.com> 2017-11-21T01:45:15Z
+
+eleventh
+------------------------------------------------------------------------
+10 | 1970-01-02T00:09:00Z | trunk
+Author: Eric Sunshine <sunshine@sunshineco.com> 2017-11-21T02:30:40Z
+Committer: Roy G. Biv <spectrum@color.com> 2017-11-21T01:45:15Z
+
+tenth
+------------------------------------------------------------------------
+EOF
+
+ $src $TESTOPTS log -v -2 tiebreak >summaryheaders.actual
+ diff summaryheaders.expect summaryheaders.actual
+ check "log -v shows summarized headers"
+
+ cat >authordate.expect <<EOF
+= tiebreak =============================================================
+11 * 2017-11-21T02:37:42Z eleventh
+10 - 2017-11-21T02:30:40Z tenth
+= tiebreak =============================================================
+11 | 2017-11-21T02:37:42Z | trunk
+eleventh
+------------------------------------------------------------------------
+10 | 2017-11-21T02:30:40Z | trunk
+tenth
+------------------------------------------------------------------------
+EOF
+
+ $src ${TESTOPTS#-T} list -2 tiebreak >authordate.actual
+ $src ${TESTOPTS#-T} log -2 tiebreak >>authordate.actual
+ diff authordate.expect authordate.actual
+ check "author date from RFC 822 header"
+
+ cat >rfc822export.expect <<EOF
+author Eric Sunshine <sunshine@sunshineco.com> 1509732768 -0500
+committer Roy G. Biv <spectrum@color.com> 1511228715 +0000
+EOF
+
+ $src ${TESTOPTS#-T} fast-export 1 tiebreak |
+ grep -E 'author|committer' >rfc822export.actual
+ diff rfc822export.expect rfc822export.actual
+ check "fast-export: consult RFC 822 headers"
+fi
+
+echo "srctest ($python $backend): all tests succeeded"
+
+rm -fr $SANDBOX
+
+# end