pvc <cmd>: patch version control command line utility.
NOTICE: pvc now works but will have bugs and the API is subject to change.
Use at your own risk and always back up work that you are using pvc
to track (aka use
git and pvc together).
pvc is a version control system similar to git or mercurial, but is ultra
simple: branches are simply a
base followed by a set of unix patches with
incrementing id. The only fundamental disadvantage (or advantage) of using pvc
instead of git is that pvc supports only fast-forward merges, which are the
cleanest and simplest to understand.
The
pvc command works very similar to git:
- initialize your repo with pvc.init.
- Whenever you have modified files you add them to
.pvcpaths (so they are tracked) and commit them
to the current branch with pvc.commit.
- When doing experimental changes, you create a branch
with pvc.branch.
- When you are ready to merge changes from your dev branch
onto your main branch:
- pvc.squash the changes to a single commit (or a few
commits).
- pvc.rebase so your branch's base is the same as main.
- pvc.grow to merge the changes in.
In the future civstack will offer tutorials for using
all the development tools.
You may also want to see
#git to use git for backups
With civ install .# you will have pvc installed.
To track an existing directory:
- cd path/to/your/project to navigate to your project
- pvc init to initialize pvc
- pvc diff --paths shows what paths pvc wants to track. Add any path
prefixes you don't want tracked to .pvcignore.
- pvc diff --paths >> .pvcpaths will update non-ignored paths.
Alternative, edit the file manually to add the paths you want
tracked.
- pvc commit -- initial pvc commit will commit your changes to
.pvc/main/commit/.../1.p.
The
.pvcignore file should contain a line-separated list of
#string.find
patterns that should be ignored. Items ending in
/ will apply to whole
directories. A common pvc ignore file might look like:
# directories
%.git/
%.out/
# extensions
%.so$
# binary files
%./path/to/some_binary
pvc is still in early development, so there is no hosting service which
ergonomically supports backing-up development. The following is how pvc
(and civstack) itself is developed and seems to work well.
Basically, we are going to have two git repositories. I will use
civstack as the example:
First, put .pvc/ in your main repo's .gitignore
echo .pvc >> .gitignore
Second, follow the #init section above. This should include adding
all your files to pvc and making your first pvc commit.
Third, cd .pvc/ and create your git repository inside the .pvc/
directory. This will literally track your patch files themselves. Use the
following as your .pvc/.gitignore. You may also want to add a README.md
directing folks to your main git repo.
# .pvc/.gitignore
**/*.snap/
backup/
Finally, add the following to your .bashrc
# Note: you must also have pvc aliased
function pvcp() {
desc="$(pvc at): $(pvc desc --full)"
(cd .pvc/ &&
git add ./ &&
git commit -am "$desc" &&
git push origin main)
}
Now you can hack using pvc commit etc and push to your repo.pvc by
simply calling pvcp. Your git commit log will be your current at
location followed by the commit message. When you want to push your
documentation or releases to git, simply do so -- your main commit log
won't be polluted by commiting pvc files.
This architecture is given both so users can debug or fix any errors as well as
to make it easier to create other implementations of pvc (i.e. in bash).
pvc is composed of the following components:
- repo: the pvc repo (repository) is stored in the .pvc/ directory inside of
a project. It contains directories (which are the branches) and the plaintext file
at which defines the "current commit" as a commit reference (i.e. branch#123).
Additionally it contains:
- backup/ directory, which contain name-<epochsec>/ directories for backups. In
general, pvc should not delete things but should instead move things to a backup
directory, reporting these operations to the user (and possibly a log file as well).
- pvcpaths is the project-local .pvcpaths file which contains a
newline-separated list of project-relative paths. This is used by pvc to
determine which paths are tracked. It's contents are tracked as a normal file
(it is included in the patch diff).
- branch: a branch is a directory inside the repo (i.e. .pvc/main/). It
contains the commit/ directory (described in commit) and the plain-text files:
- base: contains branch#123. This file is not present if the branch is
the trunk.
- tip: contains an ascii decimal number, representing the last commit id.
- branch (action): to "create a branch" means to create a new directory
inside .pvc/ and initialize it with the proper base and tip files.
The base branch must already exist.
- commit (noun): refers to a single patch file (i.e. .pvc/branch/commit/.../123.p).
- The length of commit/.../ is stored in commit/depth which is an ascii
decimal number, always divisible by 2. Each sub-directory has exactly two
digits. For instance, a depth of 4 would store 12.p in
commit/00/00/12.p and store 123456.p in 12/34/123456.p.
- description: the top of the patch file (before the first unidiff)
contains a plain-text description of the commit.
- diffs: the rest of the patch file contains a series of file differences
from the previous patch version in the unidiff (aka diff -u) format.
- commit is often shorthand for the commit reference (i.e.
branch#123), which refers uniquely to a specific branch and patch file
or snapshot directory.
- commit (action): "making a commit" means to take the difference of the
current directory and store it as a patch file in the branch's commit/
directory.
- snapshot: a snapshot is the local directory state at a specific commit. It
is a directory which uses the extension .snap/ inside of the commit/
directory, i.e. commit/00/123.snap/.
- checkout (action): to "checkout a commit" means to make the local project
directory the same as the commit. This is performed by finding the closest
snapshot and applying commit patches (either forwards or backwards) in
order to make the snapshot reflect the commit's state.
- rebase (action): to "rebase a branch" means to increase the id of it's
base. This is accomplished by making a copy of the new id's snapshot and
repeatedly applying the unix merge command (or equivalent) on each change,
using the copied snapshot as to and incrementing the base along the
change patches. Each new patch file should be stored, incrementing from
the base.
- the software should detect if conflicts are unresolveable and exit, telling the
user how to fix them. The software should be able to resume the rebase once
the conflicts are resolved.
- For example, the reference implementation creates a new branch called
branch__rebase to perform this action. When calling rebase, it first
checks for this branch and attempts to resume from it. On failure,
it tells the user where the failing files that need to be fixed are
located.
- when the rebase is complete, the old branch should be moved to .pvc/backup
then replaced with the rebased version.
- merge: merges a branch onto another one. The branch must already be
rebased to the tip (also called a "fast forward merge"), so this is literally
just copying the patch files and incrementing the tip.
- squash: combines multiple commits into one, moving larger commits down.
The descriptions should be concatenated, and can be edited separately by the
user.
- export: simply copies a branch without it's snapshot directories to a
separate directory, which can be sent to a maintainer to be merged.
Other Operations
Other operations, such as showing commit messages or ammending a commit, are
not defined explicitly, but you can see the reference implementation for
details. Typically their implementation is either straightforward or can be
performed by variations of the above operations.
Also, operations which mutate the meaning of a commit (such as squash or
rebase) should check to make sure that no branches depend on the branch being
mutated.
Usage:
pvc <subcmd> --help
This is the
pvc command-line tool which can also be called directly from
Lua.
Usage:
pvc init dir --branch=main
Initialize the current directory as a pvc directory.
This will create
.pvc/ and
.pvcpaths
Arguments:
- branch ="main"
the initial branch name
Usage:
pvc diff branch1=at branch2=local
Show the difference between two branches. The default is
at (the
current commit) vs
local (the local working directory).
This command accepts either branch#id names or paths/to/dir/, where
that dir/ has a .pvcpaths file.
Arguments:
- paths
show only changed paths
Usage:
pvc commit -- my message
Commit local changes to the current branch, creating
a new id.
If no message is given this uses the COMMIT file
for the message, if it exists.
Usage:
pvc at branchId --hard
If
branchId is not given, just returns current branch#id.
Otherwise, sets the active branch#id, causing the local
directory to be updated to be that content.
This will , this will fail (unless force=true) if it would
cause any local changes to be overwritten.
at branch is called a "checkout" in other version
control systems.
Arguments:
- force
overwrite local changes.
If given without branch, resets to current commit
Usage:
pvc tip [branch=current]
Get the tip id of branch.
Usage:
pvc branch name [from=current]
Start new branch
name branching off of
from.
If from is a path/to/dir then it will graft
those changes into the local repo as the named branch.
(often used by maintainers to accept patches).
Usage:
pvc show [branch#id] --before=10
Show the commits before/after
branch#id.
If branch#id is not given, print all branches.
Arguments:
- before
number of records before id to show
- after
number of records after id to show
- paths
show only paths.
Usage:
pvc desc branch#id=current [$to/new.cxt]
Get or set the description for a single branch id.
The new description can be passed via to/new.cxt or
after -- (like commit).
Usage:
pvc squash [name#id]
Combine changes and descriptions from
branch id -> endId (inclusive)
into a single commit. You can then edit the description using
pvc desc branch#id.
This enables making lots of small commits and then
"squashing" them into a single commit once they are
in a good state.
Arguments:
- branch ="current"
the branch to squash
Usage:
rebase [branch=current] --id=10
Change the base of
branch to
id.
Rebase is a fundamental operation. It allows you to make or accept changes
on your "main" branch while you work on development branches which are
behind the main. See #basic for more details.
Arguments:
- id
the id of base to change to
Usage:
grow --branch=current [from]
Copy changes in
from onto
branch.
In other version control systems this is called a
"fast forward merge"
Arguments:
Usage:
prune branch#id
- if #id: delete ids id -> tip (inclusive).
- else: delete branch
Usage:
export branch to/
Copy all patch files in the branch to
to/.
The resulting directory is commonly sent to
tar -zcvf branch.tar.gz path/ and then branch.tar.gz sent to a
maintainer to be merged.
Usage:
snap [branch#id=current]
Get the snapshot directory of branch#id.
The snapshot contains a copy of files at that commit.
Types: Diff
Functions
- fn branchDir(P, branch, dot)
return the branch path in project regardless of whether it exists
- fn depth(bdir)
- fn snapDir(bdir, id) -> string?
Get the snap/ path regardless of whether it exists
- fn snapshot(P, br,id) -> .../id.snap/
Snapshot the branch#id by applying patches.
Return the snapshot directory
- fn atId(P, nbr,nid)
get or set where the working id is at.
- fn resolve(P, branch) -> br, id, bdir
resolve a branch name. It can be one of:
- A directory with / in it.
- branch or branch#id
- Special: at
- fn resolveSnap(P, branch) -> snap/, br, id, bdir
resolve and take snapshot, permits local
- fn resolve2(P, br1, br2) -> branch1/ branch2/
resolve two branches into their branch directories. Defaults:
- fn nameId(P, branch,id) -> br,id
get the conventional brName, id for a branch,id pair
- fn branches(P) -> list
get all branches
- fn backupDir(P, name) -> string
return a backup directory (uses the timestamp)
Diff:of(dir1, dir2) returns what changed between two pvc dirs.
Fields:
Methods
Functions
- fn diff(a,al, b,bl) -> string?
Get the unified diff using unix diff --unified=1,
properly handling file creation/deleting
the l variables are the "label" to use.
when the coresponding value is nil then the label is /dev/null
- fn merge(to, base, change) -> ok, err
incorporate all changes that went into going from base to change into to