diff options
author | Dmitry Bogatov <KAction@debian.org> | 2018-11-10 03:09:43 +0000 |
---|---|---|
committer | Dmitry Bogatov <KAction@debian.org> | 2018-11-10 03:09:43 +0000 |
commit | 486f4254b69321ca468f4349c8f8384a651c03ae (patch) | |
tree | 0518772cc17a0754d7b22ba16486dd64b2419fae |
New upstream version 1.20
-rw-r--r-- | COPYING | 27 | ||||
-rw-r--r-- | FAQ.asc | 228 | ||||
-rw-r--r-- | INSTALL | 22 | ||||
-rw-r--r-- | Makefile | 66 | ||||
-rw-r--r-- | NEWS | 173 | ||||
-rw-r--r-- | README | 21 | ||||
-rw-r--r-- | TODO | 6 | ||||
-rw-r--r-- | control | 24 | ||||
-rwxr-xr-x | src | 2757 | ||||
-rw-r--r-- | src.1 | 311 | ||||
-rw-r--r-- | src.asc | 313 | ||||
-rwxr-xr-x | srctest | 1512 |
12 files changed, 5460 insertions, 0 deletions
@@ -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. @@ -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. @@ -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" @@ -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. @@ -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 @@ -0,0 +1,6 @@ + SRC TODO list + +* Eliminate 'L' status, turning it to '=' and automatically checking + out the file when required. + + @@ -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 + @@ -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: @@ -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)\&. @@ -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). @@ -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 |