Version 15 of Subversion

Updated 2022-03-09 21:54:16 by SEH

Subversion "SVN" is a version control system, see http://subversion.apache.org/ and http://en.wikipedia.org/wiki/Subversion_(software )

Currently (2015-12) at version 1.9.2

See also: http://tortoisesvn.tigris.org - Subversion client


tkcvs as of version 8.0 now supports Subversion in addition to CVS.


CrowTDE has subversion integration in version 0.5.


RLH 2018-02-06 - Since you can use any language to make subversion hook scripts, I wonder if any Tcl'ers have done so and would share.


SEH 20220309 -- Subversion has the (I think) unique property that arbitrary subsets of files and directories can be imported into and exported out of arbitrary locations in a Subversion repository file tree. This contrasts with e.g. git and fossil where you typically have to download, check out and sync the entire project in order to do work.

Subversion also allows you to dump arbitrary subsets of a repository containing arbitrary ranges of commits. You can use such a dump to create a new repository, use the new repository for arbitrary development tasks, then dump all or part of the repository and re-integrate the dump to an arbitrary location in the original repository.

Because of these features (among others), I've been using Subversion not as a SCM tool but as a backup/archive/versioned file store utility. I've always wanted a tool that would allow me to craft a lifetime personal archive (see the FILTR), that was independent of computer architecture or operating system, that could accommodate the many computing devices I've used in work and personal environments without being tied to any of them. Something that would allow me both to deploy and retrieve work to/from heterogeneous systems, and to collate, organize, annotate and introspect my work over long time scales. Subversion is the tool that comes closest to letting me do that. Other tools I've tried have been too inflexible/unportable/buggy/immature.

The one thing Subversion doesn't do more or less out of the box, is automatically merge an arbitrary directory into an arbitrary Subversion repository location, taking into account all edits, file deletes and new files. So I wrote the code below to do just that. Subversion comes with a perl script that purports to do the job, but it's old, thousands of lines long, and has crufted into virtual uselessness. New Subversion features added in the past decade or so have allowed me to make short work of the task.

svnsync.tcl:

#!/usr/bin/env tclsh

# Copyright (c) 2021 Stephen E. Huntley <[email protected]>
# License: Tcl License

namespace eval ::svnsync {

#################################################################

variable syntax {$svnsync PATH URL [-log_message <TEXT>] [-autoconfirm]}

variable help {

PATH: Local writable directory to be turned into a Subversion working
        checkout.

URL: Subversion repository URL. Repository subdirectory will be created within
        base repository location if it doesn't exist.

-log_message: Text to be used for log entry when commmitting changes.

-autoconfirm: Don't ask user for confirmation before final commit of changes.
        Runs the command non-interactively.

------------------------

Syncs the contents of any local directory to any location within a Subversion
repository.  Adds and deletes files within the repository as necessary.

The local checkout directory is left unchanged; this includes final deletion of
the .svn subdirectory, so the local directory is no longer a valid Subversion
working checkout when the command completes.

The checkout status is output before committing, so the user knows what changes
are to be made to the repository.  User is given a chance to abort before
committing changes, unless the -autoconfirm option is specified.

Updates and merges are beyond the scope of this command.  If necessary they
must be done independently by the user before executing svnsync.  If desired,
the user may kill the program (e.g., ctrl-c) at the point of commit
confirmation, in which case the local directory will remain as a Subversion
working checkout and the user can make changes using Subversion's tools.  The
user may then commit, or (after deleting the .svn subdirectory) run svnsync
again.

}
#################################################################

proc help {} {
        variable syntax
        variable help
        
        set svnsync svnsync
        if {[file tail $::argv0] eq {svnsync.tcl}} {
                set svnsync svnsync.tcl
        }
        
        puts "syntax: [subst -nocom $syntax]\n"
        puts [string trim $help]
}

proc deleteExtraFiles {existing_list} {
        upvar PATH PATH

        set fileListReverse [lreverse [split [exec svn list $PATH -R] \n]]

        lappend dirList
        lappend checkoutFileList
        foreach flr $fileListReverse {
                set flr [file join $PATH $flr]
                if {[file isdir $flr]} {lappend dirList $flr ; continue}
                if { ($flr ni $existing_list) && [file exists $flr] } {
                        lappend checkoutFileList $flr
                }
        }

        if {[llength $checkoutFileList]} {
                puts "Extra checked out files; to be removed:"
                puts [join $checkoutFileList \n]\n
        }
        
        foreach cfl $checkoutFileList {
                exec svn delete [file join $PATH $cfl@]
        }

        foreach dir $dirList {
                set dir [file join $PATH $dir]
                if {[glob -nocomplain $dir/*] eq {}} {
                        exec svn delete $dir@
                }
        }
}

proc svnsync {args} {

        if {2 > [llength $args] || [llength $args] > 5} {
                help
                return
        }

        set log_message "svn sync [clock format [clock seconds]]"

        #set args $::argv

        if {[set autoconfirm [lsearch $args -a*]] > -1} {
                set autoconfirm_val [lindex $args $autoconfirm]
                if {[string first $autoconfirm_val {-autoconfirm}]} {
                        error "Incorrect flag: $autoconfirm_val . Must be -autoconfirm or -log_message"
                }
                set args [lreplace $args $autoconfirm $autoconfirm]
                set autoconfirm 1
        } else {
                set autoconfirm 0
        }

        if {![expr {!([llength $args]%2)}]} {
                error "Incorrect arguments: $args"
        }

        set lkey [dict keys $args {-l*}]
        if {[string first $lkey {-log_message}]} {
                error "Incorrect argument: $lkey . Must be -autoconfirm or -log_message"
        }
        set log_message [dict get $args $lkey]
        set args [dict remove $args $lkey]

        lassign $args PATH URL

        if {"$URL$PATH" in [list $URL $PATH {}]} {
                error "Missing argument(s): must specify URL and checkout dir: $URL $PATH"
        }

        set PATH [file norm $PATH]
        if {![file isdir $PATH]} {
                error "Checkout value is not a valid directory: $PATH"
        }

        puts "autoconfirm: $autoconfirm\nlog message: $log_message\nPATH: $PATH\nURL: $URL\n"

        if {[file exists [file join $PATH .svn]]} {
                error ".svn directory exists, delete before proceeding."
        }

        catch {exec svn mkdir $URL --parents -m "svn sync add directory"}

        try {
        #############

                set co_output [exec svn checkout $URL $PATH --force]
                set co_output_list [split $co_output \n]

                unset -nocomplain existing_list
                lappend existing_list
                foreach coo $co_output_list {
                        if {[string index $coo 0] eq {E}} {
                                lappend existing_list [file norm [string trimleft [string range $coo 1 end]]]
                        }
                }

                deleteExtraFiles $existing_list

                exec svn add $PATH --force --auto-props

                set status [exec svn status $PATH]

                if {[string trim $status] eq {}} {
                        puts "No changes to commit."
                        return
                }

                puts Status:
                puts $status\n\n

                if {!$autoconfirm} {
                        puts -nonewline "Proceed? (Y/n): "
                        flush stdout
                        if {[gets stdin] ni {Y y {} } } {
                        puts        "Aborted."
                                        return
                        }
                }

                exec svn commit $PATH -m $log_message --non-interactive

                file delete -force [file join $PATH .svn]

                puts "Subversion sync complete."

        #############
        } finally {

                if {[file isdir [file join $PATH .svn]]} {
                        if {![info exists existing_list]} {
                                puts "Something went wrong with Subversion checkout.  Clean up workspace before trying again."
                        } else {
                                deleteExtraFiles $existing_list
                                file delete -force [file join $PATH .svn]
                        }
                }

        }

}

namespace export -clear svnsync

}; ####################### end namespace ::svnsync

namespace eval :: {namespace import ::svnsync::svnsync}

if {[file tail $::argv0] eq {svnsync.tcl}} {
        if {[catch {svnsync {*}$::argv} err]} {
                puts stderr $err
                set svnsync svnsync.tcl
                puts stderr "\nsyntax: [subst -nocom $::svnsync::syntax]"
                puts stderr "Type 'svnsync.tcl help' for more."
                exit 1
        }
}