├── .gitignore
├── doc
├── git-Subversion_bridge.png
├── README-repo.txt
├── README-undo-git-breakage.txt
└── synchronize-git-svn.sh.log
├── .gitmodules
├── scripts
├── setup-svn-branch-git-bridge.sh.config
├── synchronize-git-svn.sh.config
├── setup-svn-branch-git-bridge.sh
└── synchronize-git-svn.sh
├── git-svn-auth-manager
├── config-just-enough
├── src
│ ├── User.cs
│ ├── AssemblyInfo.cs
│ ├── Config.cs
│ ├── EncryptedUserRepository.cs
│ ├── SQLiteTypeAffinity.cs
│ ├── EmailSender.cs
│ ├── EncryptedSQLiteDb.cs
│ ├── GitSvnAuthManager.cs
│ ├── Main.cs
│ ├── SQLiteNativeMethods.cs
│ └── Options.cs
├── mail-sample.txt
├── GitSvnAuthManager.sln
├── config-full
├── Makefile
├── GitSvnAuthManager.csproj
└── README.rst
├── test
└── run-test.sh
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | *.swp
2 | *.dll
3 | *.exe
4 | bin/
5 | obj/
6 | *.pidb
7 | *.userprefs
8 | *.db
9 |
--------------------------------------------------------------------------------
/doc/git-Subversion_bridge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrts/git-svn-bridge/HEAD/doc/git-Subversion_bridge.png
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "git-svn-auth-manager/sqlcipher"]
2 | path = git-svn-auth-manager/sqlcipher
3 | url = git://github.com/sqlcipher/sqlcipher.git
4 |
--------------------------------------------------------------------------------
/scripts/setup-svn-branch-git-bridge.sh.config:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | BASEDIR="$HOME/git"
4 |
5 | BRANCHNAME="1.x"
6 | SVN_REPO_URL="svn://localhost/branches/${BRANCHNAME}"
7 |
8 | SYNCHRONIZE_SCRIPT="$HOME/bin/synchronize-git-svn.sh"
9 | GIT_SVN_AUTH_MANAGER="$HOME/bin/git-svn-auth-manager"
10 |
--------------------------------------------------------------------------------
/git-svn-auth-manager/config-just-enough:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/doc/README-repo.txt:
--------------------------------------------------------------------------------
1 | This repository is checked out at the remote end.
2 |
3 | Do a
4 |
5 | sudo -u git git reset --hard
6 |
7 | at the remote end to see the updates after a push.
8 |
9 | See
10 | http://stackoverflow.com/questions/5531309/reset-hard-on-git-push or
11 | http://utsl.gen.nz/git/post-update
12 | for an automated solution.
13 |
--------------------------------------------------------------------------------
/scripts/synchronize-git-svn.sh.config:
--------------------------------------------------------------------------------
1 | # The REPOS array contains paths to one or more git-svn bridge repositories
2 |
3 | REPOS=("/home/git-svn-bridge/git/git-svn-bridge-trunk")
4 |
5 | # Multiple repos example:
6 | #
7 | # TRUNK_REPO_PATH=/change/this
8 | # MAINTENANCE_BRANCH_REPO_PATH=/change/this/too
9 | # REPOS=("$TRUNK_REPO_PATH" "$MAINTENANCE_BRANCH_REPO_PATH")
10 |
--------------------------------------------------------------------------------
/doc/README-undo-git-breakage.txt:
--------------------------------------------------------------------------------
1 | Reset central repo's history to what's currently in SVN:
2 |
3 | git-svn-gateway.git$ git svn fetch
4 | git-svn-gateway.git$ git checkout master
5 | git-svn-gateway.git$ git reset --hard svn/git-svn
6 | git-svn-gateway.git$ git push git-central-repo master
7 |
8 | (probably fails, so
9 | git-svn-gateway.git$ git push --force git-central-repo master)
10 |
--------------------------------------------------------------------------------
/git-svn-auth-manager/src/User.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Data;
3 |
4 | namespace GitSvnAuthManager
5 | {
6 | internal sealed class User
7 | {
8 | public string svn_username { get; set; }
9 |
10 | public string email { get; set; }
11 |
12 | public string name { get; set; }
13 |
14 | public string svn_password { get; set; }
15 |
16 | public User (string svn_username)
17 | {
18 | this.svn_username = svn_username;
19 | }
20 |
21 | public User (DataRow record)
22 | {
23 | this.svn_username = (string)record ["svn_username"];
24 | this.email = (string)record ["email"];
25 | this.name = (string)record ["name"];
26 | this.svn_password = (string)record ["svn_password"];
27 | }
28 | }
29 | }
30 |
31 |
--------------------------------------------------------------------------------
/git-svn-auth-manager/mail-sample.txt:
--------------------------------------------------------------------------------
1 | From: ---@gmail.com
2 | To: ---@gmail.com
3 | Subject: [git-svn-auth-manager] SVN ACCESS ERROR
4 |
5 | Hi John Doe!
6 |
7 | An error occurred while accessing SVN with your credentials.
8 | Either your credentials are wrong or the SVN repository is down.
9 |
10 | If your password has changed, then please update it with
11 |
12 | git-svn-auth-manager --change-passwd-for john
13 |
14 | in the git-svn bridge host or ask for help from the person who manages it.
15 |
16 | Details:
17 | --------------------------------------------------------------------------
18 | Error executing `svn info --username "john" --password "*****" "/tmp"`:
19 | svn: '/tmp' is not a working copy
20 |
21 | --------------------------------------------------------------------------
22 |
23 | Best,
24 | git-svn-auth-manager
25 |
--------------------------------------------------------------------------------
/git-svn-auth-manager/GitSvnAuthManager.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 11.00
3 | # Visual Studio 2010
4 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitSvnAuthManager", "GitSvnAuthManager.csproj", "{DFD3B6F9-CC1B-4E1B-BEF7-F8D05C2F3D7A}"
5 | EndProject
6 | Global
7 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
8 | Debug|x86 = Debug|x86
9 | Release|x86 = Release|x86
10 | EndGlobalSection
11 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
12 | {DFD3B6F9-CC1B-4E1B-BEF7-F8D05C2F3D7A}.Debug|x86.ActiveCfg = Debug|x86
13 | {DFD3B6F9-CC1B-4E1B-BEF7-F8D05C2F3D7A}.Debug|x86.Build.0 = Debug|x86
14 | {DFD3B6F9-CC1B-4E1B-BEF7-F8D05C2F3D7A}.Release|x86.ActiveCfg = Release|x86
15 | {DFD3B6F9-CC1B-4E1B-BEF7-F8D05C2F3D7A}.Release|x86.Build.0 = Release|x86
16 | EndGlobalSection
17 | GlobalSection(MonoDevelopProperties) = preSolution
18 | StartupItem = GitSvnAuthManager.csproj
19 | EndGlobalSection
20 | EndGlobal
21 |
--------------------------------------------------------------------------------
/git-svn-auth-manager/src/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 |
4 | // Information about this assembly is defined by the following attributes.
5 | // Change them to the values specific to your project.
6 |
7 | [assembly: AssemblyTitle("git/Subversion authentication and user info manager")]
8 | [assembly: AssemblyDescription("")]
9 | [assembly: AssemblyConfiguration("")]
10 | [assembly: AssemblyCompany("")]
11 | [assembly: AssemblyProduct("git-svn-auth-manager")]
12 | [assembly: AssemblyCopyright("Mart Sõmermaa")]
13 | [assembly: AssemblyTrademark("")]
14 | [assembly: AssemblyCulture("")]
15 |
16 | // The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}".
17 | // The form "{Major}.{Minor}.*" will automatically update the build and revision,
18 | // and "{Major}.{Minor}.{Build}.*" will update just the revision.
19 |
20 | [assembly: AssemblyVersion("1.0.*")]
21 |
22 | // The following attributes are used to specify the signing key for the assembly,
23 | // if desired. See the Mono documentation for more information about signing.
24 |
25 | //[assembly: AssemblyDelaySign(false)]
26 | //[assembly: AssemblyKeyFile("")]
27 |
28 |
--------------------------------------------------------------------------------
/git-svn-auth-manager/config-full:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/git-svn-auth-manager/src/Config.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Configuration;
3 | using System.Reflection;
4 | using System.IO;
5 |
6 | namespace GitSvnAuthManager
7 | {
8 | internal static class Config
9 | {
10 | private static readonly Lazy _config = new Lazy (() => {
11 | ExeConfigurationFileMap config_map = new ExeConfigurationFileMap ();
12 | config_map.ExeConfigFilename = Path.Combine (ConfigDir, "config");
13 |
14 | return ConfigurationManager.OpenMappedExeConfiguration (config_map, ConfigurationUserLevel.None);
15 | });
16 | private static readonly Lazy _config_dir = new Lazy (() => {
17 | string appdata_dir = Environment.GetFolderPath (Environment.SpecialFolder.ApplicationData);
18 | object[] attrs = System.Reflection.Assembly.GetExecutingAssembly ().GetCustomAttributes (typeof(AssemblyProductAttribute), false);
19 | // in general attrs could be null or Length == 0, but we have the set in AssemblyInfo.cs
20 | string product_name = ((AssemblyProductAttribute)attrs [0]).Product;
21 |
22 | return Path.Combine (appdata_dir, product_name);
23 | });
24 |
25 | internal interface IConfigKeyValueCollection
26 | {
27 | string this [string key] {
28 | get;
29 | }
30 | }
31 |
32 | private class SettingsHelper : IConfigKeyValueCollection
33 | {
34 | private readonly KeyValueConfigurationCollection _settings;
35 |
36 | public SettingsHelper (KeyValueConfigurationCollection settings)
37 | {
38 | _settings = settings;
39 | }
40 |
41 | public string this [string key] {
42 | get {
43 | var element = _settings [key];
44 | return element == null ? null : element.Value;
45 | }
46 | }
47 | }
48 |
49 | public static string ConfigDir {
50 | get { return _config_dir.Value; }
51 | }
52 |
53 | public static IConfigKeyValueCollection Settings {
54 | get { return new SettingsHelper (_config.Value.AppSettings.Settings); }
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/scripts/setup-svn-branch-git-bridge.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # sudo su git
4 |
5 | set -u
6 | set -x
7 | set -e
8 |
9 | SCRIPT_FULL_PATH="$(cd "$(dirname "$0")" && pwd)/$(basename "$0")"
10 |
11 | # ----------------------
12 | # --- CONFIGURATION ----
13 | # ----------------------
14 |
15 | # Configuration is sourced from scriptname.config
16 |
17 | CONFIGFILE="${SCRIPT_FULL_PATH}.config"
18 |
19 | if [[ ! -r "$CONFIGFILE" ]]
20 | then
21 | echo "Config file $CONFIGFILE does not exist or is unreadable" >&2
22 | exit 1
23 | fi
24 |
25 | . $CONFIGFILE
26 |
27 |
28 | # ----------------------
29 | # --- IMPLEMENTATION ---
30 | # ----------------------
31 |
32 | cd "$BASEDIR"
33 |
34 | CENTRAL_REPO="${BASEDIR}/central-repo-${BRANCHNAME}.git"
35 | BRIDGE_REPO="${BASEDIR}/git-svn-bridge-${BRANCHNAME}.git"
36 |
37 | # central repo
38 |
39 | git init --bare "$CENTRAL_REPO"
40 |
41 | pushd "$CENTRAL_REPO"
42 |
43 | git remote add svn-bridge "$BRIDGE_REPO"
44 |
45 | popd
46 |
47 | # git-SVN bridge
48 |
49 | git svn --prefix=svn/ init "$SVN_REPO_URL" "$BRIDGE_REPO"
50 |
51 | pushd "$BRIDGE_REPO"
52 |
53 | git svn --authors-prog="$GIT_SVN_AUTH_MANAGER" --log-window-size 10000 fetch
54 | git remote add git-central-repo "$CENTRAL_REPO"
55 | git push --all git-central-repo
56 |
57 | popd
58 |
59 | # central repo hooks
60 |
61 | pushd "$CENTRAL_REPO"
62 |
63 | cat > hooks/update << 'EOT'
64 | #!/bin/bash
65 | set -u
66 | refname=$1
67 | shaold=$2
68 | shanew=$3
69 |
70 | # we are only interested in commits to master
71 | [[ "$refname" = "refs/heads/master" ]] || exit 0
72 |
73 | # don't allow non-fast-forward commits
74 | if [[ $(git merge-base "$shanew" "$shaold") != "$shaold" ]]; then
75 | echo "Non-fast-forward commits to master are not allowed"
76 | exit 1
77 | fi
78 | EOT
79 |
80 | cat > hooks/post-update << EOT
81 | #!/bin/bash
82 |
83 | # trigger synchronization only on commit to master
84 | for arg in "\$@"; do
85 | if [[ "\$arg" = "refs/heads/master" ]]; then
86 | $SYNCHRONIZE_SCRIPT GIT_HOOK
87 | exit \$?
88 | fi
89 | done
90 | EOT
91 |
92 | chmod 755 hooks/{update,post-update}
93 |
94 | ./hooks/post-update refs/heads/master
95 |
96 | popd
97 |
--------------------------------------------------------------------------------
/git-svn-auth-manager/Makefile:
--------------------------------------------------------------------------------
1 | BINDIR = bin
2 | OBJDIR = obj
3 | TARGETNAME = git-svn-auth-manager
4 |
5 | TARGET = $(BINDIR)/$(TARGETNAME)
6 |
7 | # Creates a bundled binary and links SQLCipher statically into it
8 | #
9 | # See http://github.com/mrts/mono-static-linking for explanations
10 | # and Mac build instructions.
11 |
12 | # C# application
13 | CSHARPC = dmcs
14 | CSHARPEXECUTABLE = $(OBJDIR)/GitSvnAuthManager.exe
15 | CSHARPREFERENCES = /r:System.dll /r:System.Data.dll /r:System.Configuration.dll
16 | CSHARPFLAGS = /nologo /warn:4 /optimize+ /codepage:utf8 /t:exe \
17 | /define:WITH_STATICALLY_LINKED_SQLCIPHER_SQLITE
18 | CHARPSRC = $(wildcard src/*.cs)
19 |
20 | # mkbundle
21 | CC = cc
22 | CFLAGS = -Wall -s -O2 -Wl,-O1
23 | GENERATEDSRC = $(OBJDIR)/hello-gen.c
24 | BUNDLEOBJS = $(OBJDIR)/hello-bundles.o
25 |
26 | # SQLCipher C library
27 | SQLCIPHERDIR = sqlcipher
28 | SQLCIPHERLIB = $(SQLCIPHERDIR)/.libs/libsqlite3.a
29 | SQLCIPHERCONFIGFLAGS = --enable-tempstore=yes \
30 | CFLAGS="-DSQLITE_HAS_CODEC -DSQLITE_ENABLE_COLUMN_METADATA" \
31 | LDFLAGS="-lcrypto"
32 |
33 | # targets
34 |
35 | all: $(TARGET)
36 |
37 | $(TARGET): $(GENERATEDSRC) $(SQLCIPHERLIB) | $(BINDIR)
38 | $(CC) -o $(TARGET) $(CFLAGS) $(GENERATEDSRC) \
39 | -rdynamic \
40 | -Wl,-whole-archive \
41 | $(SQLCIPHERLIB) \
42 | -Wl,-no-whole-archive \
43 | $(BUNDLEOBJS) \
44 | `pkg-config --cflags --libs mono-2` \
45 | `pkg-config --libs openssl`
46 |
47 | $(GENERATEDSRC): $(CSHARPEXECUTABLE)
48 | mkbundle -c -o $(GENERATEDSRC) -oo $(BUNDLEOBJS) $(CSHARPEXECUTABLE)
49 |
50 | $(SQLCIPHERLIB): $(SQLCIPHERDIR)/Makefile
51 | cd $(SQLCIPHERDIR); \
52 | make -j 4
53 |
54 | $(SQLCIPHERDIR)/Makefile: $(SQLCIPHERDIR)/configure
55 | cd $(SQLCIPHERDIR); \
56 | ./configure $(SQLCIPHERCONFIGFLAGS)
57 |
58 | $(CSHARPEXECUTABLE): $(CHARPSRC) | $(OBJDIR)
59 | $(CSHARPC) "/out:$(CSHARPEXECUTABLE)" \
60 | $(CSHARPREFERENCES) $(CSHARPFLAGS) $(CHARPSRC)
61 |
62 | install: $(TARGET)
63 | install -m 711 -D $(TARGET) ~/bin/$(TARGETNAME)
64 |
65 | install_config: install
66 | install -m 600 -D config-just-enough ~/.config/$(TARGETNAME)/config
67 |
68 | $(OBJDIR):
69 | mkdir -p $(OBJDIR)
70 |
71 | $(BINDIR):
72 | mkdir -p $(BINDIR)
73 |
74 | clean:
75 | rm -rf $(OBJDIR) $(BINDIR)
76 |
77 | mrproper: clean
78 | rm -f ~/.config/$(TARGETNAME)/userinfo.db
79 | cd $(SQLCIPHERDIR); git clean -fdx
80 |
--------------------------------------------------------------------------------
/git-svn-auth-manager/GitSvnAuthManager.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Debug
5 | x86
6 | 10.0.0
7 | 2.0
8 | {DFD3B6F9-CC1B-4E1B-BEF7-F8D05C2F3D7A}
9 | Exe
10 | GitSvnAuthManager
11 | GitSvnAuthManager
12 |
13 |
14 | true
15 | full
16 | false
17 | bin\Debug
18 | DEBUG;
19 | prompt
20 | 4
21 | x86
22 | true
23 |
24 |
25 | none
26 | false
27 | bin\Release
28 | prompt
29 | 4
30 | x86
31 | true
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/doc/synchronize-git-svn.sh.log:
--------------------------------------------------------------------------------
1 | =>
2 | trap successful
3 | ----------------------------------------------
4 | ______________________________________________
5 |
6 | Synchronizing repo '/home/git-svn-bridge/git/git-svn-bridge-trunk'
7 | Start: Thu Aug 16 23:05:00 EEST 2012
8 | ......................................
9 |
10 | =>
11 | git svn fetch --use-log-author successful
12 | ----------------------------------------------
13 | Already on 'master'
14 | =>
15 | git checkout master successful
16 | ----------------------------------------------
17 | From ../git-central-repo-trunk
18 | * branch master -> FETCH_HEAD
19 | Current branch master is up to date.
20 | =>
21 | git pull --rebase git-central-repo master successful
22 | ----------------------------------------------
23 | Note: checking out 'svn/git-svn'.
24 |
25 | You are in 'detached HEAD' state. You can look around, make experimental
26 | changes and commit them, and you can discard any commits you make in this
27 | state without impacting any branches by performing another checkout.
28 |
29 | If you want to create a new branch to retain commits you create, you may
30 | do so (now or later) by using -b with the checkout command again. Example:
31 |
32 | git checkout -b new_branch_name
33 |
34 | HEAD is now at 0cedfba... 2012-08-16 22:26:28 +0300 | Use template filters to represent amounts in localized format [cecilia] 2012-08-16 22:26:28 +0300 | Use template tag library for localized date format [cecilia] 2012-08-16 22:26:28 +0300 | Add template tag library [cecilia] 2012-08-16 19:55:40 +0300 | Update main.cpp [Git-SVN Bridge (GIT SIDE)]
35 | =>
36 | git checkout svn/git-svn successful
37 | ----------------------------------------------
38 | Already up-to-date.
39 | =>
40 | git merge --no-ff --no-commit master successful
41 | ----------------------------------------------
42 | # Not currently on any branch.
43 | nothing to commit (working directory clean)
44 | =>
45 | git commit --author bob@company.com -am <> successful
46 | ----------------------------------------------
47 | Committing to file:///home/git-svn-bridge/svn/svn-repo/trunk ...
48 | =>
49 | git svn dcommit successful
50 | ----------------------------------------------
51 | Switched to branch 'master'
52 | =>
53 | git checkout master successful
54 | ----------------------------------------------
55 | Already up-to-date.
56 | =>
57 | git merge svn/git-svn successful
58 | ----------------------------------------------
59 | Everything up-to-date
60 | =>
61 | git push git-central-repo successful
62 | ----------------------------------------------
63 | End: Thu Aug 16 23:05:01 EEST 2012
64 | ______________________________________________
65 |
66 |
--------------------------------------------------------------------------------
/git-svn-auth-manager/src/EncryptedUserRepository.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Data;
4 |
5 | namespace GitSvnAuthManager
6 | {
7 | internal sealed class EncryptedUserRepository
8 | {
9 | // Encryption key is intentionally compiled into the binary
10 | // providing additional security-by-obscurity protection for the database;
11 | // see the main README.rst for the rationale
12 | private const string ENCRYPTION_KEY = "CHANGETHIS";
13 | private readonly EncryptedSQLiteDb _db;
14 | private static readonly Lazy _instance = new Lazy (() => new EncryptedUserRepository ());
15 |
16 | public static EncryptedUserRepository Instance
17 | { get { return _instance.Value; } }
18 |
19 | private EncryptedUserRepository ()
20 | {
21 | string db_filename = Config.Settings ["db_filename"];
22 |
23 | if (db_filename == null) {
24 | string configdir = Config.ConfigDir;
25 |
26 | if (!Directory.Exists (configdir))
27 | Directory.CreateDirectory (configdir);
28 |
29 | db_filename = Path.Combine (configdir, "userinfo.db");
30 | }
31 |
32 | _db = new EncryptedSQLiteDb (db_filename);
33 |
34 | _db.ExecuteUpdate (String.Format ("PRAGMA key='{0}'", ENCRYPTION_KEY.Replace ('\'', '.')));
35 |
36 | _db.ExecuteUpdate ("CREATE TABLE IF NOT EXISTS user" +
37 | "(svn_username TEXT UNIQUE NOT NULL, " +
38 | "email TEXT UNIQUE NOT NULL, " +
39 | "name TEXT NOT NULL, " +
40 | "svn_password TEXT NOT NULL)");
41 | }
42 |
43 | public void Save (User user)
44 | {
45 | _db.ExecuteUpdate ("INSERT OR REPLACE INTO user (svn_username, email, name, svn_password)" +
46 | "VALUES (?, ?, ?, ?)", user.svn_username, user.email, user.name, user.svn_password);
47 | }
48 |
49 | public User LoadBySvnUsername (string svn_username)
50 | {
51 | using (DataTable records = _db.ExecuteQuery ("SELECT * FROM user WHERE svn_username = ?", svn_username)) {
52 | return LoadFromDataTable (records, "user " + svn_username);
53 | }
54 | }
55 |
56 | public User LoadByEmail (string email)
57 | {
58 | using (DataTable records = _db.ExecuteQuery ("SELECT * FROM user WHERE email = ?", email)) {
59 | return LoadFromDataTable (records, "email " + email);
60 | }
61 | }
62 |
63 | private User LoadFromDataTable (DataTable records, string entity)
64 | {
65 | if (records.Rows.Count == 0)
66 | throw new ApplicationException (Char.ToUpper (entity [0]) + entity.Substring (1) + " not in database");
67 | if (records.Rows.Count > 1)
68 | throw new ApplicationException ("Multiple records of " + entity + "in database");
69 |
70 | return new User (records.Rows [0]);
71 | }
72 | }
73 | }
74 |
75 |
--------------------------------------------------------------------------------
/git-svn-auth-manager/src/SQLiteTypeAffinity.cs:
--------------------------------------------------------------------------------
1 | //
2 | // Adopted from Mono.Data.Sqlite.SQLiteConvert.cs
3 | //
4 | // Author(s):
5 | // Robert Simpson (robert@blackcastlesoft.com)
6 | //
7 | // Adapted and modified for the Mono Project by
8 | // Marek Habersack (grendello@gmail.com)
9 | //
10 | //
11 | // Copyright (C) 2006 Novell, Inc (http://www.novell.com)
12 | // Copyright (C) 2007 Marek Habersack
13 | //
14 | // Permission is hereby granted, free of charge, to any person obtaining
15 | // a copy of this software and associated documentation files (the
16 | // "Software"), to deal in the Software without restriction, including
17 | // without limitation the rights to use, copy, modify, merge, publish,
18 | // distribute, sublicense, and/or sell copies of the Software, and to
19 | // permit persons to whom the Software is furnished to do so, subject to
20 | // the following conditions:
21 | //
22 | // The above copyright notice and this permission notice shall be
23 | // included in all copies or substantial portions of the Software.
24 | //
25 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
26 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
27 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
28 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
29 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
30 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
31 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
32 | //
33 |
34 | /********************************************************
35 | * ADO.NET 2.0 Data Provider for Sqlite Version 3.X
36 | * Written by Robert Simpson (robert@blackcastlesoft.com)
37 | *
38 | * Released to the public domain, use at your own risk!
39 | ********************************************************/
40 | namespace GitSvnAuthManager
41 | {
42 | ///
43 | /// Sqlite has very limited types, and is inherently text-based. The first 5 types below represent the sum of all types Sqlite
44 | /// understands. The DateTime extension to the spec is for internal use only.
45 | ///
46 | public enum TypeAffinity
47 | {
48 | ///
49 | /// Not used
50 | ///
51 | Uninitialized = 0,
52 | ///
53 | /// All integers in Sqlite default to Int64
54 | ///
55 | Int64 = 1,
56 | ///
57 | /// All floating point numbers in Sqlite default to double
58 | ///
59 | Double = 2,
60 | ///
61 | /// The default data type of Sqlite is text
62 | ///
63 | Text = 3,
64 | ///
65 | /// Typically blob types are only seen when returned from a function
66 | ///
67 | Blob = 4,
68 | ///
69 | /// Null types can be returned from functions
70 | ///
71 | Null = 5
72 | }
73 |
74 | }
75 |
--------------------------------------------------------------------------------
/git-svn-auth-manager/src/EmailSender.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net.Mail;
3 | using System.Net;
4 | using System.Security.Cryptography.X509Certificates;
5 | using System.Net.Security;
6 |
7 | namespace GitSvnAuthManager
8 | {
9 | internal static class EmailSender
10 | {
11 | private const string SMTP_SERVER_HOST_DEFAULT = "smtp.gmail.com";
12 | private const string SMTP_SERVER_PORT_DEFAULT = "587";
13 | private const string MAIL_SUBJECT_DEFAULT = "[{0}] SVN ACCESS ERROR";
14 | private const string MAIL_BODY_DEFAULT = @"Hi {0}!
15 |
16 | An error occurred while accessing SVN with your credentials.
17 | Either your credentials are wrong or the SVN repository is down.
18 |
19 | If your password has changed, then please update it with
20 |
21 | {1} --change-passwd-for {2}
22 |
23 | in the git-svn bridge host or ask for help from the person who manages it.
24 |
25 | Details:
26 | --------------------------------------------------------------------------
27 | {3}
28 | --------------------------------------------------------------------------
29 |
30 | Best,
31 | {1}";
32 |
33 | public static bool IsEmailValid (string email)
34 | {
35 | try {
36 | new MailAddress (email);
37 | } catch (FormatException) {
38 | return false;
39 | }
40 | return true;
41 | }
42 |
43 | public static bool SendErrorEmail (User user, string error)
44 | {
45 | var settings = Config.Settings;
46 |
47 | if ((settings ["mail_sending_enabled"] ?? "false") == "false")
48 | return false;
49 |
50 | string smtp_username = settings ["smtp_username"];
51 | string smtp_password = settings ["smtp_password"];
52 |
53 | string smtp_server_host = settings ["smtp_server_host"] ?? SMTP_SERVER_HOST_DEFAULT;
54 | string smtp_server_port = settings ["smtp_server_port"] ?? SMTP_SERVER_PORT_DEFAULT;
55 |
56 | string mail_from = settings ["mail_from"] ?? smtp_username;
57 | string mail_subject = settings ["mail_subject"] ?? String.Format (MAIL_SUBJECT_DEFAULT, MainClass.APP_NAME);
58 | string mail_body = settings ["mail_body"] ?? MAIL_BODY_DEFAULT;
59 |
60 | MailMessage message = new MailMessage (mail_from, user.email, mail_subject,
61 | String.Format (mail_body, user.name, MainClass.APP_NAME,
62 | user.svn_username, error));
63 |
64 | SmtpClient smtp = new SmtpClient (smtp_server_host, Convert.ToInt32 (smtp_server_port));
65 | smtp.Credentials = new NetworkCredential (smtp_username, smtp_password);
66 | smtp.EnableSsl = true;
67 |
68 | // importing the GMail certificate is a hassle, see http://stackoverflow.com/a/9803922
69 | if ((settings ["do_not_check_server_certificate"] ?? "true") == "true")
70 | ServicePointManager.ServerCertificateValidationCallback = delegate(object s, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) {
71 | return true; };
72 |
73 | smtp.Send (message);
74 | return true;
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/git-svn-auth-manager/src/EncryptedSQLiteDb.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Data;
3 | using System.IO;
4 | using System.Runtime.InteropServices;
5 |
6 | namespace GitSvnAuthManager
7 | {
8 | // TODO: IDisposable
9 | internal sealed class EncryptedSQLiteDb
10 | {
11 | #if WITH_STATICALLY_LINKED_SQLCIPHER_SQLITE
12 | #warning PREPARING CODE FOR STATICALLY LINKED SQLIPHER SQLITE
13 | // To make the runtime lookup symbols in the current executable
14 | // when using static linking,
15 | // use the special library name "__Internal"
16 | internal const string SQLITE_DLL = "__Internal";
17 | #else
18 | public const string SQLITE_DLL = "sqlite3";
19 | #endif
20 | private IntPtr _db = IntPtr.Zero;
21 |
22 | public EncryptedSQLiteDb (string db_filename)
23 | {
24 | if (SQLiteNativeMethods.sqlite3_open16 (db_filename, out _db) != SQLITE_OK)
25 | throw new ApplicationException ("Failed to open database " + db_filename);
26 | }
27 |
28 | ~EncryptedSQLiteDb ()
29 | {
30 | if (_db != IntPtr.Zero)
31 | SQLiteNativeMethods.sqlite3_close (_db);
32 | }
33 |
34 | public DataTable ExecuteQuery (string query, params string[] args)
35 | {
36 | IntPtr statement = PrepareStatement (query, args);
37 |
38 | DataTable result = new DataTable ();
39 |
40 | try {
41 | int column_count = SQLiteNativeMethods.sqlite3_column_count (statement);
42 |
43 | for (int i = 0; i < column_count; i++) {
44 | IntPtr col_name = SQLiteNativeMethods.sqlite3_column_origin_name16 (statement, i);
45 | result.Columns.Add (UniPtrToString (col_name), typeof(string));
46 | }
47 |
48 | while (SQLiteNativeMethods.sqlite3_step(statement) == SQLITE_ROW) {
49 | string[] row = new string[column_count];
50 |
51 | for (int i = 0; i < column_count; i++) {
52 | if (SQLiteNativeMethods.sqlite3_column_type (statement, i) != TypeAffinity.Text)
53 | throw new ApplicationException (String.Format ("Column {0} is not string", i));
54 | IntPtr val = SQLiteNativeMethods.sqlite3_column_text16 (statement, i);
55 | row [i] = UniPtrToString (val) ?? "";
56 | }
57 |
58 | result.Rows.Add (row);
59 | }
60 |
61 | } finally {
62 | FinalizeStatement (statement);
63 | }
64 |
65 | return result;
66 | }
67 |
68 | public void ExecuteUpdate (string query, params string[] args)
69 | {
70 | IntPtr statement = PrepareStatement (query, args);
71 | try {
72 | if (SQLiteNativeMethods.sqlite3_step (statement) != SQLITE_DONE)
73 | throw new ApplicationException ("Execute update result not DONE: " + ErrMsg ());
74 | } finally {
75 | FinalizeStatement (statement);
76 | }
77 | }
78 |
79 | private string UniPtrToString (IntPtr str)
80 | {
81 | return Marshal.PtrToStringUni (str);
82 | }
83 |
84 | private string ErrMsg ()
85 | {
86 | IntPtr msg_ptr = SQLiteNativeMethods.sqlite3_errmsg16 (_db);
87 | return UniPtrToString (msg_ptr) ?? "";
88 | }
89 |
90 | private IntPtr PrepareStatement (string query, params string[] args)
91 | {
92 | IntPtr statement;
93 |
94 | if (SQLiteNativeMethods.sqlite3_prepare16_v2 (_db, query, query.Length * 2, out statement, IntPtr.Zero) != SQLITE_OK)
95 | throw new ApplicationException (ErrMsg ());
96 |
97 | for (int i = 1; i <= args.Length; i++) {
98 | string arg = args [i - 1];
99 | if (SQLiteNativeMethods.sqlite3_bind_text16 (statement, i, arg, arg.Length * 2, -1) != SQLITE_OK)
100 | throw new ApplicationException (ErrMsg ());
101 | }
102 |
103 | return statement;
104 | }
105 |
106 | private void FinalizeStatement (IntPtr statement)
107 | {
108 | if (SQLiteNativeMethods.sqlite3_finalize (statement) != SQLITE_OK)
109 | throw new ApplicationException (ErrMsg ());
110 | }
111 |
112 | private const int SQLITE_OK = 0;
113 | private const int SQLITE_ROW = 100;
114 | private const int SQLITE_DONE = 101;
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/git-svn-auth-manager/README.rst:
--------------------------------------------------------------------------------
1 | git/Subversion authentication and user information manager
2 | ==========================================================
3 |
4 | Helper utility for running a git-SVN bridge.
5 |
6 | - Manages SVN authentication for git
7 | - User mapping between git and SVN
8 | - Keeps data in an encrypted database
9 | - Sends email notifications when SVN authentication fails
10 |
11 | Database is in::
12 |
13 | ~/.config/git-svn-auth-manager/userinfo.db
14 |
15 | Usage
16 | -----
17 |
18 | Run either with a single non-option argument to output user
19 | name and email suitable for ``git --authors-prog``::
20 |
21 | git-svn-auth-manager USERNAME
22 |
23 | or with a single option to invoke option-specific behaviour::
24 |
25 | git-svn-auth-manager OPTION=USERNAME [ARG]
26 |
27 | Options::
28 |
29 | --help, -h Show help
30 |
31 | --add-user, -a=USERNAME
32 | Add user information to the database
33 |
34 | --change-passwd-for, -p=USERNAME
35 | Change user's password in the database
36 |
37 | --reset-auth-for, -r=USERNAME
38 | Reset SVN auth cache with user's credentials;
39 | SVN URL required as non-option argument
40 |
41 | Building
42 | --------
43 |
44 | Clone with ``--recursive``::
45 |
46 | $ git clone --recursive git://github.com/mrts/git-svn-bridge.git
47 |
48 | Install build deps::
49 |
50 | $ sudo apt-get install build-essential mono-devel libssl-dev
51 |
52 | **Change key**, build::
53 |
54 | $ cd git-svn-bridge/git-svn-auth-manager/GitSvnAuthManager
55 | $ ENCRYPTION_KEY=`tr -dc '[:alnum:]' < /dev/urandom | head -c 16`
56 | $ sed -i "s/CHANGETHIS/$ENCRYPTION_KEY/" src/EncryptedUserRepository.cs
57 | $ make
58 |
59 | Security
60 | --------
61 |
62 | Database is encrypted, see SQLCipher.
63 |
64 | As encryption key is embedded in ``git-svn-auth-manager``, it needs to be owned
65 | by root and be made execute-only::
66 |
67 | $ sudo chown root: git-svn-auth-manager
68 | $ chmod 711 git-svn-auth-manager
69 |
70 |
71 | Configuration
72 | -------------
73 |
74 | Config is in::
75 |
76 | ~/.config/git-svn-auth-manager/config
77 |
78 | Configuration settings (from ``git-svn-auth-manager -h``)::
79 |
80 | svn_auth_dir: SVN authentication cache folder
81 | (default: ${HOME}/.subversion/auth)
82 |
83 | db_filename: encrypted user info database location
84 | (default: ${ApplicationData}/git-svn-auth-manager/userinfo.db,
85 | ${ApplicationData} is ${HOME}/.config in Linux)
86 |
87 | mail_sending_enabled: if "true", send error mails to users
88 | when `svn info` fails (default: false);
89 | if mail_sending_enabled is true,
90 | the following additional settings apply:
91 |
92 | smtp_username: SMTP username (NO DEFAULT)
93 |
94 | smtp_password: SMTP password (NO DEFAULT)
95 |
96 | smtp_server_host: SMTP server host name (default: smtp.gmail.com)
97 |
98 | smtp_server_port: SMTP server port (default: 587)
99 |
100 | mail_from: mail From: header (default: ${smtp_username})
101 |
102 | mail_subject: mail Subject: header
103 | (default: built-in ${MAIL_SUBJECT_DEFAULT})
104 |
105 | mail_body: mail message body, must have {}-placeholders for
106 | user name, application name, SVN username and error message
107 | (default: built-in ${MAIL_BODY_DEFAULT})
108 |
109 | do_not_check_server_certificate: if "true", do not check SMTP
110 | server certificate
111 | (default: true, i.e. certificate is NOT checked)
112 |
113 |
114 | See GitSvnAuthManager.exe.config-full for a full sample or
115 | GitSvnAuthManager.exe.config-sensible for enabling mail sending (other settings
116 | can be left to defaults if GMail is used).
117 |
118 | Mail sending
119 | ++++++++++++
120 |
121 | See mail-sample.txt for the mail template that is used by default.
122 |
123 | Enable email notifications to users for *Subversion* authentication failures
124 | (**substitute sed replacment strings with real GMail account data**)::
125 |
126 | $ sed -i 's/username@gmail.com/REAL_GMAIL_USER/' GitSvnAuthManager.exe.config
127 | $ sed -i 's/password/REAL_GMAIL_PASSWORD/' GitSvnAuthManager.exe.config
128 |
129 | If you feel uneasy about keeping mail usernames/passwords in config file,
130 | write them directly into src/EmailSender.cs and recompile::
131 |
132 | $ sed -i 's/settings ["smtp_username"]/"REAL_GMAIL_USER"/' src/EmailSender.cs
133 | $ sed -i 's/settings ["smtp_password"]/"REAL_GMAIL_PASSWORD"/' src/EmailSender.cs
134 | $ make
135 |
--------------------------------------------------------------------------------
/git-svn-auth-manager/src/GitSvnAuthManager.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Configuration;
4 | using System.Diagnostics;
5 | using System.Text.RegularExpressions;
6 |
7 | namespace GitSvnAuthManager
8 | {
9 | internal static class GitSvnAuthManager
10 | {
11 | public static void AddUser (string svn_username)
12 | {
13 | var user = new User (svn_username);
14 |
15 | Console.WriteLine ("Adding/overwriting SVN user " + svn_username);
16 |
17 | user.svn_password = GetPassword ();
18 | user.email = GetFromConsole ("Email");
19 | if (!EmailSender.IsEmailValid (user.email))
20 | throw new ApplicationException ("Invalid email: " + user.email);
21 | user.name = GetFromConsole ("Name");
22 |
23 | EncryptedUserRepository.Instance.Save (user);
24 | }
25 |
26 | public static void ChangePassword (string svn_username)
27 | {
28 | var user = EncryptedUserRepository.Instance.LoadBySvnUsername (svn_username);
29 |
30 | Console.WriteLine ("Changing SVN password for SVN user " + user.svn_username);
31 |
32 | user.svn_password = GetPassword ();
33 |
34 | EncryptedUserRepository.Instance.Save (user);
35 | }
36 |
37 | // Output "User Name " for given SVN username
38 | // for using the application for `git --authors-prog`.
39 | public static void OutputForGitAuthorsProg (string svn_username)
40 | {
41 | var user = EncryptedUserRepository.Instance.LoadBySvnUsername (svn_username);
42 |
43 | Console.WriteLine ("{0} <{1}>", user.name, user.email);
44 | }
45 |
46 | // Reset SVN auth cache by running
47 | // `svn info --username "$USERNAME" --password "$PASSWORD" "$SVN_URL"`,
48 | // and send email to the user on failure (if mail sending is enabled)
49 | public static void ResetSubversionAuthCache (string email, string svn_url)
50 | {
51 | if (!EmailSender.IsEmailValid (email))
52 | throw new ApplicationException ("Invalid email: " + email);
53 |
54 | var user = EncryptedUserRepository.Instance.LoadByEmail (email);
55 |
56 | string svn_auth_dir = Config.Settings ["svn_auth_dir"] ??
57 | Path.Combine (Environment.GetFolderPath (Environment.SpecialFolder.Personal),
58 | ".subversion", "auth");
59 |
60 | string svn_auth_dir_backup = svn_auth_dir + "." + MainClass.APP_NAME + "-backup";
61 | if (Directory.Exists (svn_auth_dir))
62 | Directory.Move (svn_auth_dir, svn_auth_dir_backup);
63 |
64 | string args = String.Format (@"info --non-interactive --username ""{0}"" --password ""{1}"" ""{2}""",
65 | QuoteForShell (user.svn_username), QuoteForShell (user.svn_password),
66 | QuoteForShell (svn_url));
67 |
68 | var svn_info_startinfo = new ProcessStartInfo ("svn", args);
69 | svn_info_startinfo.CreateNoWindow = true;
70 | svn_info_startinfo.UseShellExecute = false;
71 | svn_info_startinfo.RedirectStandardOutput = true;
72 | svn_info_startinfo.RedirectStandardError = true;
73 |
74 | using (Process svn_info = Process.Start (svn_info_startinfo)) {
75 | string stdout = svn_info.StandardOutput.ReadToEnd ();
76 | string stderr = svn_info.StandardError.ReadToEnd ();
77 | // TODO: timeout?
78 | svn_info.WaitForExit ();
79 |
80 | if (svn_info.ExitCode != 0) {
81 | string error = "Error executing `svn " +
82 | Regex.Replace (args, @"--password ""[^""]+""", @"--password ""*****""") + "`:\n"
83 | + stdout + stderr;
84 | try {
85 | bool sending_successful = EmailSender.SendErrorEmail (user, error);
86 | if (sending_successful)
87 | Console.Error.WriteLine (MainClass.APP_NAME + ": error email sent");
88 | } catch (Exception e) {
89 | MainClass.ShowError (e, "Error while sending email: ");
90 | }
91 |
92 | // restore auth directory from backup or error
93 | Directory.Delete (svn_auth_dir, true);
94 | Directory.Move (svn_auth_dir_backup, svn_auth_dir);
95 |
96 | throw new ApplicationException (error);
97 | } else {
98 | Directory.Delete (svn_auth_dir_backup, true);
99 | }
100 | }
101 | }
102 |
103 | private static string GetFromConsole (string field)
104 | {
105 | Console.Write (field + ": ");
106 | string response = Console.ReadLine ();
107 |
108 | if (String.IsNullOrEmpty (response))
109 | throw new ApplicationException (field + " cannot be empty");
110 |
111 | return response;
112 | }
113 |
114 | private static string GetPassword ()
115 | {
116 | string passwd1 = GetFromConsole ("SVN password");
117 | string passwd2 = GetFromConsole ("SVN password (confirm)");
118 |
119 | if (passwd1 != passwd2)
120 | throw new ApplicationException ("Passwords don't match");
121 |
122 | return passwd1;
123 | }
124 |
125 | private static string QuoteForShell (string arg)
126 | {
127 | return arg.Replace ("\\", "\\\\").Replace ("\"", "\\\"");
128 | }
129 | }
130 | }
131 |
132 |
--------------------------------------------------------------------------------
/git-svn-auth-manager/src/Main.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Collections.Generic;
4 | using System.Configuration;
5 |
6 | using Mono.Options;
7 |
8 | namespace GitSvnAuthManager
9 | {
10 | static class MainClass
11 | {
12 | internal const string APP_NAME = "git-svn-auth-manager";
13 | private const string OPT_ADD_USER = "add_user";
14 | private const string OPT_CHANGE_PASSWD = "change_passwd";
15 | private const string OPT_RESET_AUTH = "reset_auth";
16 |
17 | public static int Main (string[] args)
18 | {
19 | bool show_help = false;
20 |
21 | var options = new Dictionary ();
22 |
23 | var option_set = new OptionSet () {
24 | {"help|h", "Show help",
25 | option => show_help = option != null},
26 | {"add-user=|a", "Add user information to the database",
27 | option => options [OPT_ADD_USER] = option},
28 | {"change-passwd-for=|p", "Change user's password in the database",
29 | option => options [OPT_CHANGE_PASSWD] = option},
30 | {"reset-auth-for=|r", "Reset SVN auth cache with user's credentials; option argument is user's email; SVN URL required as non-option argument",
31 | option => options [OPT_RESET_AUTH] = option}
32 | };
33 |
34 | List non_option_arguments;
35 | try {
36 | non_option_arguments = option_set.Parse (args);
37 | } catch (Exception e) {
38 | ShowError (e);
39 | Console.Error.WriteLine ();
40 | ShowHelp (false, option_set);
41 | return 1;
42 | }
43 |
44 | if (show_help) {
45 | ShowHelp (true, option_set);
46 | return 0;
47 | }
48 |
49 | if (!(options.Count == 1 && non_option_arguments.Count <= 1 // RESET_AUTH takes SVN_URL as non-option argument
50 | || options.Count == 0 && non_option_arguments.Count == 1)) {
51 | Console.Error.WriteLine (APP_NAME + ": too few, too many or invalid arguments\n");
52 | ShowHelp (false, option_set);
53 | return 1;
54 | }
55 |
56 | if (options.Count == 0 && non_option_arguments.Count == 1) {
57 | string non_option_argument = non_option_arguments [0];
58 | if (non_option_argument.StartsWith ("-")) {
59 | Console.Error.WriteLine (APP_NAME + ": unknown option " + non_option_argument + "\n");
60 | ShowHelp (false, option_set);
61 | return 1;
62 | }
63 |
64 | return ExecuteAction (delegate {
65 | GitSvnAuthManager.OutputForGitAuthorsProg (non_option_argument);
66 | });
67 | }
68 |
69 | string the_option = new List (options.Keys) [0];
70 |
71 | if (the_option != OPT_RESET_AUTH && non_option_arguments.Count == 1
72 | || the_option == OPT_RESET_AUTH && non_option_arguments.Count == 0) {
73 | Console.Error.WriteLine (APP_NAME + ": A non-option argument is required for `reset-auth-for`, but forbidden for other options\n");
74 | ShowHelp (false, option_set);
75 | return 1;
76 | }
77 |
78 | if (the_option == OPT_RESET_AUTH && non_option_arguments.Count == 1)
79 | return ExecuteAction (delegate {
80 | GitSvnAuthManager.ResetSubversionAuthCache (options [the_option], non_option_arguments [0]);
81 | });
82 |
83 | // => the_option != RESET_AUTH && non_option_arguments.Count == 0
84 |
85 | var option_handlers = new Dictionary> () {
86 | {OPT_ADD_USER, GitSvnAuthManager.AddUser},
87 | {OPT_CHANGE_PASSWD, GitSvnAuthManager.ChangePassword}
88 | };
89 |
90 | return ExecuteAction (delegate {
91 | option_handlers [the_option] (options [the_option]); });
92 | }
93 |
94 | internal static void ShowError (Exception e, string message = "")
95 | {
96 | Console.Error.WriteLine (APP_NAME + ": " + message +
97 | e.GetType ().ToString () + ": " + e.Message);
98 | }
99 |
100 | private static int ExecuteAction (Action action)
101 | {
102 | try {
103 | action ();
104 | } catch (Exception e) {
105 | ShowError (e);
106 | return 1;
107 | }
108 | return 0;
109 | }
110 |
111 | private static void ShowHelp (bool help_requested, OptionSet options)
112 | {
113 | TextWriter writer = help_requested ? Console.Out : Console.Error;
114 |
115 | writer.WriteLine (String.Format (
116 | @"Helper utility for running a git-SVN bridge.
117 | Manages SVN authentication for git and user mapping between git and SVN.
118 |
119 | Usage:
120 | either with a single non-option argument to output user
121 | name and email suitable for `git --authors-prog`:
122 |
123 | {0} SVN_USERNAME
124 |
125 | or with a single option to add users or change passwords:
126 |
127 | {0} OPTION=SVN_USERNAME
128 |
129 | or with a single option and single non-option argument to reset
130 | SVN authentication cache:
131 |
132 | {0} --reset-auth-for=EMAIL SVN_URL
133 |
134 | Options:", APP_NAME));
135 |
136 | options.WriteOptionDescriptions (writer);
137 |
138 | if (help_requested) {
139 | writer.WriteLine (String.Format (
140 | @"Configuration settings:
141 |
142 | svn_auth_dir: SVN authentication cache folder
143 | (default: ${{HOME}}/.subversion/auth)
144 |
145 | db_filename: encrypted user info database location
146 | (default: ${{ApplicationData}}/{0}/userinfo.db,
147 | ${{ApplicationData}} is ${{HOME}}/.config in Linux)
148 |
149 | mail_sending_enabled: if ""true"", send error mails to users
150 | when `svn info` fails (default: false);
151 | if mail_sending_enabled is true,
152 | the following additional settings apply:
153 |
154 | smtp_username: SMTP username (NO DEFAULT)
155 |
156 | smtp_password: SMTP password (NO DEFAULT)
157 |
158 | smtp_server_host: SMTP server host name (default: smtp.gmail.com)
159 |
160 | smtp_server_port: SMTP server port (default: 587)
161 |
162 | mail_from: mail From: header (default: ${{smtp_username}})
163 |
164 | mail_subject: mail Subject: header
165 | (default: built-in ${{MAIL_SUBJECT_DEFAULT}})
166 |
167 | mail_body: mail message body, must have {{}}-placeholders for
168 | user name, application name, SVN username and error message
169 | (default: built-in ${{MAIL_BODY_DEFAULT}})
170 |
171 | do_not_check_server_certificate: if ""true"", do not check SMTP
172 | server certificate
173 | (default: true, i.e. certificate is NOT checked)
174 | ", APP_NAME));
175 | }
176 | }
177 | }
178 | }
179 |
--------------------------------------------------------------------------------
/scripts/synchronize-git-svn.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # IT IS VITALLY IMPORTANT THAT LINE ENDINGS ARE CONSISTENTLY SET IN THE
4 | # MIRROR REPO -- AS IT DOES A MERGE AND MESSES THINGS UP OTHERWISE
5 | #
6 | # For a Windows-only project:
7 | #
8 | # git config core.eol crlf
9 | # git config core.autocrlf false
10 | # git config core.safecrlf true
11 | # git config core.whitespace cr-at-eol
12 | #
13 | # Avoid problems with case-sensitive files:
14 | #
15 | # git config core.ignorecase true
16 |
17 | # TODO:
18 | #
19 | # function handle_error()
20 | # {
21 | # if mail has not yet been sent
22 | # create guard for $1
23 | # echo "$1 failed, see $LOGFILE"
24 | # else if mail has been sent once for this error (i.e. guard exists)
25 | # update count for $1 guard to 2 or add marker
26 | # echo remainder and say that no more notices
27 | # will be echoed until the guard has been removed
28 | # else don't echo anything
29 | #
30 | # exit 1
31 | # }
32 |
33 | TRIGGERED_BY=${1:-NONE}
34 |
35 | set -u
36 |
37 | SCRIPT_FULL_PATH="$(cd "$(dirname "$0")" && pwd)/$(basename "$0")"
38 |
39 | # ----------------------
40 | # --- CONFIGURATION ----
41 | # ----------------------
42 |
43 | # Configuration is sourced from scriptname.config
44 |
45 | CONFIGFILE="${SCRIPT_FULL_PATH}.config"
46 |
47 | if [[ ! -r "$CONFIGFILE" ]]
48 | then
49 | echo "Config file $CONFIGFILE does not exist or is unreadable" >&2
50 | exit 1
51 | fi
52 |
53 | . $CONFIGFILE
54 |
55 | # ----------------------
56 | # --- IMPLEMENTATION ---
57 | # ----------------------
58 |
59 | function error_exit()
60 | {
61 | local THECMD="$1"
62 | local LOGFILE="$2"
63 |
64 | echo "$THECMD FAILED" >> "$LOGFILE"
65 | echo "$THECMD failed, see $LOGFILE" >&2
66 | exit 1
67 | }
68 |
69 | function check_status()
70 | {
71 | LAST_STATUS=$?
72 |
73 | local THECMD="$1"
74 | local LOGFILE="$2"
75 |
76 | echo "=>" >> "$LOGFILE"
77 | [[ $LAST_STATUS = 0 ]] || error_exit "$THECMD" "$LOGFILE"
78 | [[ $LAST_STATUS = 0 ]] && echo "$THECMD successful" >> "$LOGFILE"
79 | echo "----------------------------------------------" >> "$LOGFILE"
80 | }
81 |
82 | function check_failure()
83 | {
84 | local THECMD="$1"
85 | local LOGFILE="$2"
86 |
87 | tail -6 "$LOGFILE" | grep 'error: git-svn died'
88 | [[ $? = 0 ]] && error_exit "$THECMD (probably wrong SVN credentials)" "$LOGFILE"
89 | tail -6 "$LOGFILE" | grep 'uthorization failed'
90 | [[ $? = 0 ]] && error_exit "$THECMD (probably wrong SVN credentials)" "$LOGFILE"
91 | }
92 |
93 | function rotate_logs()
94 | {
95 | local LOGFILE="$1"
96 |
97 | if [[ -e "$LOGFILE" ]]; then
98 | for i in 3 2 1; do
99 | FROM="${LOGFILE}.${i}"
100 | TO="${LOGFILE}.$((i + 1))"
101 | [[ -e "$FROM" ]] && cp "$FROM" "$TO"
102 | done
103 | cp "$LOGFILE" "${LOGFILE}.1"
104 | fi
105 |
106 | > "$LOGFILE"
107 | }
108 |
109 | function release_lock()
110 | {
111 | local LOCKDIR="$1"
112 |
113 | for toplevel_dir in /*; do
114 | if [[ "$LOCKDIR" = "$toplevel_dir" ]]; then
115 | echo "Refusing to rm -rf $LOCKDIR" >&2
116 | exit 1
117 | fi
118 | done
119 |
120 | rm -rf "$LOCKDIR"
121 | }
122 |
123 | function acquire_lock()
124 | {
125 | # Inspired by http://wiki.bash-hackers.org/howto/mutex
126 | # and http://wiki.grzegorz.wierzowiecki.pl/code:mutex-in-bash
127 |
128 | local LOCKDIR="$1"
129 | local PIDFILE="${LOCKDIR}/pid"
130 |
131 | if mkdir "$LOCKDIR" &>/dev/null; then
132 | # lock succeeded
133 |
134 | # remove $LOCKDIR on exit
135 | trap 'release_lock "$LOCKDIR"' EXIT \
136 | || { echo 'trap exit failed' >&2; exit 1; }
137 |
138 | # will trigger the EXIT trap above by `exit`
139 | trap 'echo "Sync script killed" >&2; exit 1' HUP INT QUIT TERM \
140 | || { echo 'trap killsignals failed' >&2; exit 1; }
141 |
142 | echo "$$" >"$PIDFILE"
143 |
144 | return 0
145 |
146 | else
147 | # lock failed, now check if the other PID is alive
148 | OTHERPID="$(cat "$PIDFILE" 2>/dev/null)"
149 |
150 | if [[ $? != 0 ]]; then
151 | # PID file does not exists - propably direcotry
152 | # is being deleted
153 | return 1
154 | fi
155 |
156 | if ! kill -0 $OTHERPID &>/dev/null; then
157 | # lock is stale, remove it and restart
158 | echo "Stale lock in sync script" >&2
159 | release_lock "$LOCKDIR"
160 | acquire_lock "$LOCKDIR"
161 | return $?
162 |
163 | else
164 | # lock is valid and OTHERPID is active - exit,
165 | # we're locked!
166 | return 1
167 | fi
168 | fi
169 | }
170 |
171 | function wait_for_lock()
172 | {
173 | local LOCKDIR="$1"
174 |
175 | # For timeout:
176 | # - local WAIT_TIMEOUT=120
177 | # - add `&& [[ $i -lt $WAIT_TIMEOUT ]]` to while condition
178 | # - add ((i++)) into while body
179 |
180 | while ! acquire_lock "$LOCKDIR"; do
181 | sleep 1
182 | done
183 | }
184 |
185 | LAST_SVN_USER_FILE="${SCRIPT_FULL_PATH}.last_svn_user"
186 |
187 | function set_subversion_user()
188 | {
189 | local AUTHOR_EMAIL="$1"
190 | local SVN_URL="$2"
191 | local LOGFILE="$3"
192 |
193 | # cache last user to avoid unneccessary git-svn-auth-manager calls
194 | [[ -e "$LAST_SVN_USER_FILE" ]] \
195 | && [[ "$(cat $LAST_SVN_USER_FILE)" == "$AUTHOR_EMAIL" ]] \
196 | && return
197 |
198 | echo -n $AUTHOR_EMAIL > $LAST_SVN_USER_FILE
199 | check_status "echo -n $AUTHOR_EMAIL > $LAST_SVN_USER_FILE" "$LOGFILE"
200 |
201 | # reset SVN auth cache with git-svn-auth-manager
202 | if ! ~/bin/git-svn-auth-manager \
203 | -r "$AUTHOR_EMAIL" "$SVN_URL" &>> "$LOGFILE"
204 | then
205 | echo "PROBLEM WITH SVN OR WITH $AUTHOR_EMAIL SVN CREDENTIALS" >&2
206 | false
207 | fi
208 | check_status "~/bin/git-svn-auth-manager -r $AUTHOR_EMAIL $SVN_URL" "$LOGFILE"
209 | }
210 |
211 | function synchronize_svn_bridge_and_central_repo()
212 | {
213 | local BRIDGE_REPO_PATH="$1"
214 | local LOGFILE="$2"
215 |
216 | pushd "$BRIDGE_REPO_PATH" > /dev/null
217 | check_status "pushd $BRIDGE_REPO_PATH" "$LOGFILE"
218 |
219 | # GIT_DIR (and possibly GIT_WORK_TREE) have to be unset,
220 | # otherwise the script will not work from post-update hook
221 | # see http://serverfault.com/questions/107608/git-post-receive-hook-with-git-pull-failed-to-find-a-valid-git-directory/107703#107703
222 | unset $(git rev-parse --local-env-vars)
223 |
224 | # get new SVN changes
225 | local AUTHORS_PROG="$HOME/bin/git-svn-auth-manager"
226 | git svn --authors-prog="$AUTHORS_PROG" fetch &>> "$LOGFILE"
227 | check_status "git svn --authors-prog=$AUTHORS_PROG fetch" "$LOGFILE"
228 | check_failure "git svn --authors-prog=$AUTHORS_PROG fetch" "$LOGFILE"
229 |
230 | # get new git changes
231 | git checkout master &>> "$LOGFILE"
232 | check_status "git checkout master" "$LOGFILE"
233 | git pull --rebase git-central-repo master &>> "$LOGFILE"
234 | check_status "git pull --rebase git-central-repo master" "$LOGFILE"
235 |
236 | # store the SVN URL and author of the last commit for `git svn dcommit`
237 | local SVN_URL=`git svn info --url`
238 | check_status "git svn info --url" "$LOGFILE"
239 | local AUTHOR_EMAIL=`git log -n 1 --format='%ae'`
240 | check_status "git log -n 1 --format='%ae'" "$LOGFILE"
241 | echo "Using SVN URL '$SVN_URL' and author email '$AUTHOR_EMAIL'" >> "$LOGFILE"
242 |
243 | # checkout detached head
244 | git checkout svn/git-svn &>> "$LOGFILE"
245 | check_status "git checkout svn/git-svn" "$LOGFILE"
246 |
247 | # create a merged log message for the merge commit to SVN
248 | # => note that we squash log messages together for merge commits
249 | # => experiment with the fomat, e.g. '%ai | [%an] %s' etc
250 | local MESSAGE=`git log --pretty=format:'%ai | %B [%an]' HEAD..master`
251 | check_status "git log --pretty=format:'%ai | %B [%an]' HEAD..master" "$LOGFILE"
252 |
253 | # merge changes from master to the SVN-tracking branch and commit to SVN
254 | # => note that we always record the merge with --no-ff
255 | git merge --no-ff --no-log -m "$MESSAGE" master &>> $LOGFILE
256 | check_status 'git merge --no-ff --no-log -m $MESSAGE master' $LOGFILE
257 |
258 | # set the SVN user and commit changes to SVN
259 | set_subversion_user "$AUTHOR_EMAIL" "$SVN_URL" "$LOGFILE"
260 | git svn --authors-prog="$AUTHORS_PROG" dcommit &>> "$LOGFILE"
261 | check_status "git svn --authors-prog=$AUTHORS_PROG dcommit" "$LOGFILE"
262 |
263 | # merge changes from the SVN-tracking branch back to master
264 | git checkout master &>> "$LOGFILE"
265 | check_status "git checkout master" "$LOGFILE"
266 | git merge svn/git-svn &>> "$LOGFILE"
267 | check_status "git merge svn/git-svn" "$LOGFILE"
268 |
269 | # fetch changes to central repo master from SVN bridge master
270 | # (note that cannot just `git push git-central-repo master`
271 | # as that would trigger the central repo update hook and deadlock)
272 | local CENTRAL_REPO_PATH="`git remote -v show | awk 'NR > 1 { exit }; { print $2 };'`"
273 | pushd "$CENTRAL_REPO_PATH" >/dev/null
274 | check_status "pushd $CENTRAL_REPO_PATH" "$LOGFILE"
275 | git fetch svn-bridge master:master &>> "$LOGFILE"
276 | check_status "git fetch svn-bridge master:master" "$LOGFILE"
277 | popd >/dev/null
278 |
279 | popd >/dev/null
280 | }
281 |
282 | LOGFILE="${SCRIPT_FULL_PATH}.log"
283 | LOCKDIR="${SCRIPT_FULL_PATH}.lock"
284 |
285 | wait_for_lock "$LOCKDIR"
286 |
287 | rotate_logs "$LOGFILE"
288 |
289 | if [[ "$TRIGGERED_BY" != "NONE" ]]
290 | then
291 | echo "..................................................." >> "$LOGFILE"
292 | echo "Triggered by $TRIGGERED_BY" >> "$LOGFILE"
293 | echo "..................................................." >> "$LOGFILE"
294 | fi
295 |
296 | for repo in ${REPOS[@]}
297 | do
298 | echo -e "______________________________________________\n" >> "$LOGFILE"
299 |
300 | if [[ -d "$repo" && -d "$repo/.git" ]] && \
301 | grep -qFx '[svn-remote "svn"]' "$repo/.git/config"
302 | then
303 | echo "Synchronizing repo '$repo'" >> "$LOGFILE"
304 | echo "Start: `date`" >> "$LOGFILE"
305 | echo -e "......................................\n" >> "$LOGFILE"
306 |
307 | synchronize_svn_bridge_and_central_repo "$repo" "$LOGFILE"
308 |
309 | echo "End: `date`" >> "$LOGFILE"
310 |
311 | else
312 | echo "Repo '$repo' does not exist or is not a git-svn repo" >&2
313 | echo "Repo '$repo' does not exist or is not a git-svn repo" >> "$LOGFILE"
314 | fi
315 |
316 | echo -e "______________________________________________\n" >> "$LOGFILE"
317 |
318 | done
319 |
--------------------------------------------------------------------------------
/test/run-test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # See ../README.md for explanations
4 | #
5 | # Please run this script with a test user account.
6 | #
7 | # NOTE THAT THE SCRIPT CHANGES SVN CONFIGURATION AS FOLLOWS:
8 | #
9 | # 'store-plaintext-passwords = yes'
10 | #
11 | # IN ~/.subversion/servers
12 | #
13 | # AND GIT CONFIGURATION AS FOLLOWS:
14 | #
15 | # git config --global user.name "Git-SVN Bridge (GIT SIDE)"
16 | # git config --global user.email "git-svn-bridge@company.com"
17 |
18 | set -u
19 |
20 | # For easy changing from command line:
21 | #
22 | # sed -i 's/GMAIL_SMTP_USERNAME="username@gmail.com/GMAIL_SMTP_USERNAME="YOURUSERNAME@gmail.com/;s/GMAIL_SMTP_PASSWORD="userpassword/GMAIL_SMTP_PASSWORD="YOURUSERPASSWORD/' run-test.sh
23 | #
24 | GMAIL_SMTP_USERNAME="username@gmail.com"
25 | GMAIL_SMTP_PASSWORD="userpassword"
26 |
27 | function die()
28 | {
29 | local THE_VARIABLE="$1"
30 | echo "Please change $THE_VARIABLE before running $0"
31 | exit 1
32 | }
33 |
34 | [[ "$GMAIL_SMTP_USERNAME" == "username@gmail.com" ]] \
35 | && die "GMAIL_SMTP_USERNAME"
36 | [[ "$GMAIL_SMTP_PASSWORD" == "userpassword" ]] \
37 | && die "GMAIL_SMTP_PASSWORD"
38 |
39 | # assure that .subversion is created
40 | svn info /tmp
41 |
42 | set -e
43 | set -x
44 |
45 | mkdir {bin,git,svn,test}
46 |
47 | # svn
48 |
49 | STORE_PASSWD='store-plaintext-passwords = yes'
50 | grep -qx "$STORE_PASSWD" ~/.subversion/servers \
51 | || echo "$STORE_PASSWD" >> ~/.subversion/servers
52 |
53 | cd ~/svn
54 | svnadmin create svn-repo
55 | svn co file://`pwd`/svn-repo svn-checkout
56 |
57 | cd svn-checkout
58 | mkdir -p trunk/src
59 | echo 'int main() { return 0; }' > trunk/src/main.cpp
60 | svn add trunk
61 | svn ci -m "First commit."
62 |
63 | # svnserve
64 |
65 | SVNSERVE_PIDFILE="$HOME/svn/svnserve.pid"
66 | SVNSERVE_LOGFILE="$HOME/svn/svnserve.log"
67 | SVNSERVE_CONFFILE="$HOME/svn/svnserve.conf"
68 | SVNSERVE_USERSFILE="$HOME/svn/svnserve.users"
69 |
70 | >> $SVNSERVE_LOGFILE
71 |
72 | cat > "$SVNSERVE_CONFFILE" << EOT
73 | [general]
74 | realm = git-SVN test
75 | anon-access = none
76 | password-db = $SVNSERVE_USERSFILE
77 | EOT
78 |
79 | cat > "$SVNSERVE_USERSFILE" << EOT
80 | [users]
81 | git-svn-bridge = git-svn-bridge
82 | alice = alice
83 | bob = bob
84 | carol = carol
85 | dave = dave
86 | EOT
87 |
88 | TAB="`printf '\t'`"
89 |
90 | cat > ~/svn/Makefile << EOT
91 | svnserve-start:
92 | ${TAB}svnserve -d \\
93 | ${TAB}${TAB}--pid-file "$SVNSERVE_PIDFILE" \\
94 | ${TAB}${TAB}--log-file "$SVNSERVE_LOGFILE" \\
95 | ${TAB}${TAB}--config-file "$SVNSERVE_CONFFILE" \\
96 | ${TAB}${TAB}-r ~/svn/svn-repo
97 |
98 | svnserve-stop:
99 | ${TAB}kill \`cat "$SVNSERVE_PIDFILE"\`
100 | EOT
101 |
102 | make -f ~/svn/Makefile svnserve-start
103 |
104 | # git
105 |
106 | git config --global user.name "Git-SVN Bridge (GIT SIDE)"
107 | git config --global user.email "git-svn-bridge@company.com"
108 |
109 | cd ~/git
110 | git init --bare git-central-repo-trunk.git
111 | cd git-central-repo-trunk.git
112 | git remote add svn-bridge ../git-svn-bridge-trunk
113 |
114 | SVN_REPO_URL="svn://localhost/trunk"
115 | cd ~/git
116 | git svn init --prefix=svn/ $SVN_REPO_URL git-svn-bridge-trunk
117 | cd git-svn-bridge-trunk
118 | AUTHORS='/tmp/git-svn-bridge-authors'
119 | echo 'git-svn-bridge = Git SVN Bridge ' > $AUTHORS
120 | echo -e "\n>>> USE 'git-svn-bridge' AS PASSWORD <<<\n"
121 | git svn fetch --authors-file="$AUTHORS" --log-window-size 10000
122 |
123 | git branch -a -v
124 |
125 | git remote add git-central-repo ../git-central-repo-trunk.git
126 | git push --all git-central-repo
127 |
128 | cd ~/git
129 | git clone git-central-repo-trunk.git git-central-repo-clone
130 | cd git-central-repo-clone
131 | git log
132 |
133 | cd ~/git/git-central-repo-trunk.git
134 | cat > hooks/update << 'EOT'
135 | #!/bin/bash
136 | set -u
137 | refname=$1
138 | shaold=$2
139 | shanew=$3
140 |
141 | # we are only interested in commits to master
142 | [[ "$refname" = "refs/heads/master" ]] || exit 0
143 |
144 | # don't allow non-fast-forward commits
145 | if [[ $(git merge-base "$shanew" "$shaold") != "$shaold" ]]; then
146 | echo "Non-fast-forward commits to master are not allowed"
147 | exit 1
148 | fi
149 | EOT
150 |
151 | cat > hooks/post-update << 'EOT'
152 | #!/bin/bash
153 |
154 | # trigger synchronization only on commit to master
155 | for arg in "$@"; do
156 | if [[ "$arg" = "refs/heads/master" ]]; then
157 | /home/git-svn-bridge/bin/synchronize-git-svn.sh GIT_HOOK
158 | exit $?
159 | fi
160 | done
161 | EOT
162 |
163 | cat > ~/bin/synchronize-git-svn.sh << 'EOT'
164 | # test script to verify that the git hook works properly
165 | echo "Commit from $1 to master" > /tmp/test-synchronize-git-svn
166 | exit 1 # test that error exit does not abort the update
167 | EOT
168 |
169 | chmod 755 hooks/update
170 | chmod 755 hooks/post-update
171 | chmod 755 ~/bin/synchronize-git-svn.sh
172 |
173 | cd ~/git/git-central-repo-clone
174 | echo "void do_nothing() { }" >> src/main.cpp
175 | git commit -am "Update main.cpp"
176 | git push
177 | less /tmp/test-synchronize-git-svn
178 |
179 | echo "void do_nothing() { }" >> src/main.cpp
180 | git add src/
181 | git commit --amend
182 | set +e
183 | git push --force
184 | set -e
185 | git reset --hard origin/master
186 |
187 | cd ~/git
188 | git clone --recursive git://github.com/mrts/git-svn-bridge.git github-git-svn-bridge-utils
189 | cd github-git-svn-bridge-utils/git-svn-auth-manager
190 | ENCRYPTION_KEY=`tr -dc '[:alnum:]' < /dev/urandom | head -c 16`
191 | sed -i "s/CHANGETHIS/$ENCRYPTION_KEY/" src/EncryptedUserRepository.cs
192 | read -p 'Should I remove the database? (y/n) ' SHOULD_REMOVE_DB
193 | [[ "$SHOULD_REMOVE_DB" = "y" ]] && make mrproper
194 | make install_config
195 | GITSVNAUTHMGRCONF="$HOME/.config/git-svn-auth-manager/config"
196 | sed -i "s/username@gmail.com/$GMAIL_SMTP_USERNAME/" "$GITSVNAUTHMGRCONF"
197 | sed -i "s/userpassword/$GMAIL_SMTP_PASSWORD/" "$GITSVNAUTHMGRCONF"
198 | set +x
199 | BRIDGE_EMAIL="git-svn-bridge@company.com"
200 | echo -e "\n>>> USE 'git-svn-bridge' AS PASSWORD AND '$BRIDGE_EMAIL' AS EMAIL <<<\n"
201 | set -x
202 | ~/bin/git-svn-auth-manager -a git-svn-bridge
203 | ~/bin/git-svn-auth-manager -r $BRIDGE_EMAIL $SVN_REPO_URL
204 | set +e
205 | echo .dump | sqlite3 ~/.config/git-svn-auth-manager/userinfo.db
206 | ~/bin/git-svn-auth-manager -r $BRIDGE_EMAIL /tmp
207 | set -e
208 |
209 | cd ~/bin
210 | cp ../git/github-git-svn-bridge-utils/scripts/synchronize-git-svn.sh .
211 | cp ../git/github-git-svn-bridge-utils/scripts/synchronize-git-svn.sh.config .
212 | ./synchronize-git-svn.sh
213 |
214 | cd ~/git/git-central-repo-clone
215 | git pull --rebase
216 | echo "void more_do_nothing() { }" >> src/main.cpp
217 | git commit -am "Add more_do_nothing() to main.cpp"
218 | git push
219 |
220 | # Actors
221 |
222 | cd ~/test
223 | mkdir {alice,bob,carol,dave}
224 |
225 | for name in alice bob; do
226 | set +x
227 | echo -e "\n>>> USE '$name' AS PASSWORD AND '$name@company.com' AS EMAIL <<<\n"
228 | set -x
229 | ~/bin/git-svn-auth-manager -a $name
230 | pushd $name
231 | svn --username $name --password $name co $SVN_REPO_URL
232 | popd
233 | done
234 |
235 | for name in carol dave; do
236 | set +x
237 | echo -e "\n>>> USE '$name' AS PASSWORD AND '$name@company.com' AS EMAIL <<<\n"
238 | set -x
239 | ~/bin/git-svn-auth-manager -a $name
240 | pushd $name
241 | git clone ~/git/git-central-repo-trunk.git git-trunk
242 | cd git-trunk
243 | git config user.name `~/bin/git-svn-auth-manager $name | sed 's/ <.*//'`
244 | git config user.email `~/bin/git-svn-auth-manager $name | sed 's/.*<\(.*\)>/\1/'`
245 | popd
246 | done
247 |
248 | make -f ~/svn/Makefile svnserve-stop
249 |
250 | cat > alice.sh << 'EOT'
251 | #!/bin/bash
252 | pushd alice/trunk
253 | echo 'void alice() { }' >> src/alice.cpp
254 | svn --username alice --password alice up
255 | svn add src/alice.cpp
256 | svn --username alice --password alice ci -m "Protect the global cache with a mutex"
257 | popd
258 | EOT
259 |
260 | cat > bob.sh << 'EOT'
261 | #!/bin/bash
262 | pushd bob/trunk
263 | echo 'void bob() { }' >> src/bob.cpp
264 | svn --username bob --password bob up
265 | svn add src/bob.cpp
266 | svn --username bob --password bob ci -m "Cache rendered templates"
267 | echo 'void bob2() { }' >> src/bob.cpp
268 | svn --username bob --password bob up
269 | svn --username bob --password bob ci -m "Add tags to articles"
270 | popd
271 | EOT
272 |
273 | cat > carol.sh << 'EOT'
274 | #!/bin/bash
275 | pushd carol/git-trunk
276 | echo 'void carol1() { }' >> src/carol.cpp
277 | git add src/carol.cpp
278 | git commit -m "Add template tag library"
279 | echo 'void carol2() { }' >> src/carol.cpp
280 | git commit -am "Use template tag library for localized date format"
281 | git pull --rebase
282 | git push
283 | echo 'void carol3() { }' >> src/carol.cpp
284 | git commit -am "Use template filters to represent amounts in localized format"
285 | git pull --rebase
286 | git push
287 | popd
288 | EOT
289 |
290 | cat > dave.sh << 'EOT'
291 | #!/bin/bash
292 | # dave is working on a task branch
293 | pushd dave/git-trunk
294 | git checkout -b payment-support
295 | echo 'void dave1() { }' >> src/dave.cpp
296 | git add src/dave.cpp
297 | git commit -m "Add payment processing interface"
298 | echo 'void dave2() { }' >> src/dave.cpp
299 | git commit -am "Implement PayPal payments"
300 | echo 'void dave3() { }' >> src/dave.cpp
301 | git commit -am "Implement credit card payments"
302 | git fetch
303 | git rebase origin/master
304 | echo 'void dave4() { }' >> src/dave.cpp
305 | git commit -am "Add storage encryption for payments"
306 | git checkout master
307 | git pull --rebase
308 | git merge --no-ff payment-support
309 | git push
310 | popd
311 | EOT
312 |
313 | cat > cron.sh << 'EOT'
314 | #!/bin/bash
315 | ~/bin/synchronize-git-svn.sh CRON
316 | EOT
317 |
318 | chmod 755 *.sh
319 |
320 | cat > Makefile << EOT
321 | all: alice bob carol dave cron
322 | .PHONY: all alice bob carol dave cron svn
323 |
324 | EOT
325 |
326 | for name in alice bob carol dave cron; do
327 | echo -e "${name}:\n\t./${name}.sh\n" >> Makefile
328 | done
329 |
330 | set +x
331 |
332 | echo
333 | echo '----------------------------------------'
334 | echo 'STAGE IS SET, RUN'
335 | echo ' cd test'
336 | echo ' make -f ~/svn/Makefile svnserve-start'
337 | echo ' make'
338 | echo ' make -f ~/svn/Makefile svnserve-stop'
339 | echo '----------------------------------------'
340 | echo
341 |
--------------------------------------------------------------------------------
/git-svn-auth-manager/src/SQLiteNativeMethods.cs:
--------------------------------------------------------------------------------
1 | //
2 | // Adopted from Mono.Data.Sqlite.UnsafeNativeMethods.cs
3 | //
4 | // Author(s):
5 | // Robert Simpson (robert@blackcastlesoft.com)
6 | //
7 | // Adapted and modified for the Mono Project by
8 | // Marek Habersack (grendello@gmail.com)
9 | //
10 | //
11 | // Copyright (C) 2006 Novell, Inc (http://www.novell.com)
12 | // Copyright (C) 2007 Marek Habersack
13 | //
14 | // Permission is hereby granted, free of charge, to any person obtaining
15 | // a copy of this software and associated documentation files (the
16 | // "Software"), to deal in the Software without restriction, including
17 | // without limitation the rights to use, copy, modify, merge, publish,
18 | // distribute, sublicense, and/or sell copies of the Software, and to
19 | // permit persons to whom the Software is furnished to do so, subject to
20 | // the following conditions:
21 | //
22 | // The above copyright notice and this permission notice shall be
23 | // included in all copies or substantial portions of the Software.
24 | //
25 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
26 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
27 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
28 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
29 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
30 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
31 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
32 | //
33 |
34 | /********************************************************
35 | * ADO.NET 2.0 Data Provider for Sqlite Version 3.X
36 | * Written by Robert Simpson (robert@blackcastlesoft.com)
37 | *
38 | * Released to the public domain, use at your own risk!
39 | ********************************************************/
40 |
41 | namespace GitSvnAuthManager
42 | {
43 | using System;
44 | using System.Security;
45 | using System.Runtime.InteropServices;
46 |
47 | #if !PLATFORM_COMPACTFRAMEWORK
48 | [SuppressUnmanagedCodeSecurity]
49 | #endif
50 | internal static class SQLiteNativeMethods
51 | {
52 | private const string SQLITE_DLL = EncryptedSQLiteDb.SQLITE_DLL;
53 |
54 | [DllImport(SQLITE_DLL)]
55 | internal static extern void sqlite3_sleep (uint dwMilliseconds);
56 |
57 | [DllImport(SQLITE_DLL)]
58 | internal static extern IntPtr sqlite3_libversion ();
59 |
60 | [DllImport(SQLITE_DLL)]
61 | internal static extern void sqlite3_free (IntPtr p);
62 |
63 | [DllImport(SQLITE_DLL)]
64 | internal static extern int sqlite3_open (byte[] utf8Filename, out IntPtr db);
65 |
66 | [DllImport(SQLITE_DLL)]
67 | internal static extern void sqlite3_interrupt (IntPtr db);
68 |
69 | [DllImport(SQLITE_DLL)]
70 | internal static extern int sqlite3_close (IntPtr db);
71 |
72 | [DllImport(SQLITE_DLL)]
73 | internal static extern int sqlite3_exec (IntPtr db, byte[] strSql, IntPtr pvCallback, IntPtr pvParam, out IntPtr errMsg, out int len);
74 |
75 | [DllImport(SQLITE_DLL)]
76 | internal static extern IntPtr sqlite3_errmsg (IntPtr db);
77 |
78 | [DllImport(SQLITE_DLL)]
79 | internal static extern int sqlite3_changes (IntPtr db);
80 |
81 | [DllImport(SQLITE_DLL)]
82 | internal static extern int sqlite3_busy_timeout (IntPtr db, int ms);
83 |
84 | [DllImport(SQLITE_DLL)]
85 | internal static extern int sqlite3_prepare_v2 (IntPtr db, IntPtr pSql, int nBytes, out IntPtr stmt, out IntPtr ptrRemain);
86 |
87 | [DllImport(SQLITE_DLL)]
88 | internal static extern int sqlite3_prepare (IntPtr db, IntPtr pSql, int nBytes, out IntPtr stmt, out IntPtr ptrRemain);
89 |
90 | [DllImport(SQLITE_DLL)]
91 | internal static extern int sqlite3_bind_blob (IntPtr stmt, int index, Byte[] value, int nSize, IntPtr nTransient);
92 |
93 | [DllImport(SQLITE_DLL)]
94 | internal static extern int sqlite3_bind_double (IntPtr stmt, int index, double value);
95 |
96 | [DllImport(SQLITE_DLL)]
97 | internal static extern int sqlite3_bind_int (IntPtr stmt, int index, int value);
98 |
99 | [DllImport(SQLITE_DLL)]
100 | internal static extern int sqlite3_bind_int64 (IntPtr stmt, int index, long value);
101 |
102 | [DllImport(SQLITE_DLL)]
103 | internal static extern int sqlite3_bind_null (IntPtr stmt, int index);
104 |
105 | [DllImport(SQLITE_DLL)]
106 | internal static extern int sqlite3_bind_text (IntPtr stmt, int index, byte[] value, int nlen, IntPtr pvReserved);
107 |
108 | [DllImport(SQLITE_DLL)]
109 | internal static extern int sqlite3_bind_parameter_count (IntPtr stmt);
110 |
111 | [DllImport(SQLITE_DLL)]
112 | internal static extern IntPtr sqlite3_bind_parameter_name (IntPtr stmt, int index);
113 |
114 | [DllImport(SQLITE_DLL)]
115 | internal static extern int sqlite3_bind_parameter_index (IntPtr stmt, byte[] strName);
116 |
117 | [DllImport(SQLITE_DLL)]
118 | internal static extern int sqlite3_column_count (IntPtr stmt);
119 |
120 | [DllImport(SQLITE_DLL)]
121 | internal static extern IntPtr sqlite3_column_name (IntPtr stmt, int index);
122 |
123 | [DllImport(SQLITE_DLL)]
124 | internal static extern IntPtr sqlite3_column_decltype (IntPtr stmt, int index);
125 |
126 | [DllImport(SQLITE_DLL)]
127 | internal static extern int sqlite3_step (IntPtr stmt);
128 |
129 | [DllImport(SQLITE_DLL)]
130 | internal static extern double sqlite3_column_double (IntPtr stmt, int index);
131 |
132 | [DllImport(SQLITE_DLL)]
133 | internal static extern int sqlite3_column_int (IntPtr stmt, int index);
134 |
135 | [DllImport(SQLITE_DLL)]
136 | internal static extern Int64 sqlite3_column_int64 (IntPtr stmt, int index);
137 |
138 | [DllImport(SQLITE_DLL)]
139 | internal static extern IntPtr sqlite3_column_text (IntPtr stmt, int index);
140 |
141 | [DllImport(SQLITE_DLL)]
142 | internal static extern IntPtr sqlite3_column_blob (IntPtr stmt, int index);
143 |
144 | [DllImport(SQLITE_DLL)]
145 | internal static extern int sqlite3_column_bytes (IntPtr stmt, int index);
146 |
147 | [DllImport(SQLITE_DLL)]
148 | internal static extern TypeAffinity sqlite3_column_type (IntPtr stmt, int index);
149 |
150 | [DllImport(SQLITE_DLL)]
151 | internal static extern int sqlite3_finalize (IntPtr stmt);
152 |
153 | [DllImport(SQLITE_DLL)]
154 | internal static extern int sqlite3_reset (IntPtr stmt);
155 |
156 | [DllImport(SQLITE_DLL)]
157 | internal static extern int sqlite3_aggregate_count (IntPtr context);
158 |
159 | [DllImport(SQLITE_DLL)]
160 | internal static extern IntPtr sqlite3_value_blob (IntPtr p);
161 |
162 | [DllImport(SQLITE_DLL)]
163 | internal static extern int sqlite3_value_bytes (IntPtr p);
164 |
165 | [DllImport(SQLITE_DLL)]
166 | internal static extern double sqlite3_value_double (IntPtr p);
167 |
168 | [DllImport(SQLITE_DLL)]
169 | internal static extern int sqlite3_value_int (IntPtr p);
170 |
171 | [DllImport(SQLITE_DLL)]
172 | internal static extern Int64 sqlite3_value_int64 (IntPtr p);
173 |
174 | [DllImport(SQLITE_DLL)]
175 | internal static extern IntPtr sqlite3_value_text (IntPtr p);
176 |
177 | [DllImport(SQLITE_DLL)]
178 | internal static extern TypeAffinity sqlite3_value_type (IntPtr p);
179 |
180 | [DllImport(SQLITE_DLL)]
181 | internal static extern void sqlite3_result_blob (IntPtr context, byte[] value, int nSize, IntPtr pvReserved);
182 |
183 | [DllImport(SQLITE_DLL)]
184 | internal static extern void sqlite3_result_double (IntPtr context, double value);
185 |
186 | [DllImport(SQLITE_DLL)]
187 | internal static extern void sqlite3_result_error (IntPtr context, byte[] strErr, int nLen);
188 |
189 | [DllImport(SQLITE_DLL)]
190 | internal static extern void sqlite3_result_int (IntPtr context, int value);
191 |
192 | [DllImport(SQLITE_DLL)]
193 | internal static extern void sqlite3_result_int64 (IntPtr context, Int64 value);
194 |
195 | [DllImport(SQLITE_DLL)]
196 | internal static extern void sqlite3_result_null (IntPtr context);
197 |
198 | [DllImport(SQLITE_DLL)]
199 | internal static extern void sqlite3_result_text (IntPtr context, byte[] value, int nLen, IntPtr pvReserved);
200 |
201 | [DllImport(SQLITE_DLL)]
202 | internal static extern IntPtr sqlite3_aggregate_context (IntPtr context, int nBytes);
203 |
204 | [DllImport(SQLITE_DLL)]
205 | internal static extern int sqlite3_table_column_metadata (IntPtr db, byte[] dbName, byte[] tblName, byte[] colName, out IntPtr ptrDataType, out IntPtr ptrCollSeq, out int notNull, out int primaryKey, out int autoInc);
206 |
207 | [DllImport(SQLITE_DLL)]
208 | internal static extern IntPtr sqlite3_column_database_name (IntPtr stmt, int index);
209 |
210 | [DllImport(SQLITE_DLL)]
211 | internal static extern IntPtr sqlite3_column_database_name16 (IntPtr stmt, int index);
212 |
213 | [DllImport(SQLITE_DLL)]
214 | internal static extern IntPtr sqlite3_column_table_name (IntPtr stmt, int index);
215 |
216 | [DllImport(SQLITE_DLL)]
217 | internal static extern IntPtr sqlite3_column_table_name16 (IntPtr stmt, int index);
218 |
219 | [DllImport(SQLITE_DLL)]
220 | internal static extern IntPtr sqlite3_column_origin_name (IntPtr stmt, int index);
221 |
222 | [DllImport(SQLITE_DLL)]
223 | internal static extern IntPtr sqlite3_column_origin_name16 (IntPtr stmt, int index);
224 |
225 | [DllImport(SQLITE_DLL)]
226 | internal static extern IntPtr sqlite3_column_text16 (IntPtr stmt, int index);
227 |
228 | [DllImport(SQLITE_DLL, CharSet = CharSet.Unicode)]
229 | internal static extern int sqlite3_open16 (string utf16Filename, out IntPtr db);
230 |
231 | [DllImport(SQLITE_DLL)]
232 | internal static extern IntPtr sqlite3_errmsg16 (IntPtr db);
233 |
234 | [DllImport(SQLITE_DLL, CharSet = CharSet.Unicode)]
235 | internal static extern int sqlite3_prepare16_v2 (IntPtr db, string pSql, int sqlLen, out IntPtr stmt, IntPtr unused);
236 |
237 | [DllImport(SQLITE_DLL, CharSet = CharSet.Unicode)]
238 | internal static extern int sqlite3_bind_text16 (IntPtr stmt, int index, string value, int nlen, int nTransient);
239 |
240 | [DllImport(SQLITE_DLL)]
241 | internal static extern IntPtr sqlite3_column_name16 (IntPtr stmt, int index);
242 |
243 | [DllImport(SQLITE_DLL)]
244 | internal static extern IntPtr sqlite3_column_decltype16 (IntPtr stmt, int index, out int len);
245 |
246 | [DllImport(SQLITE_DLL)]
247 | internal static extern IntPtr sqlite3_value_text16 (IntPtr p);
248 |
249 | [DllImport(SQLITE_DLL, CharSet = CharSet.Unicode)]
250 | internal static extern void sqlite3_result_error16 (IntPtr context, string strName, int nLen);
251 |
252 | [DllImport(SQLITE_DLL, CharSet = CharSet.Unicode)]
253 | internal static extern void sqlite3_result_text16 (IntPtr context, string strName, int nLen, IntPtr pvReserved);
254 |
255 | [DllImport(SQLITE_DLL, CharSet = CharSet.Unicode, SetLastError = true)]
256 | internal static extern int sqlite3_encryptfile (string fileName);
257 |
258 | [DllImport(SQLITE_DLL, CharSet = CharSet.Unicode, SetLastError = true)]
259 | internal static extern int sqlite3_decryptfile (string fileName);
260 |
261 | [DllImport(SQLITE_DLL, CharSet = CharSet.Unicode, SetLastError = true)]
262 | internal static extern int sqlite3_encryptedstatus (string fileName, out int fileStatus);
263 |
264 | [DllImport(SQLITE_DLL, CharSet = CharSet.Unicode, SetLastError = true)]
265 | internal static extern int sqlite3_compressfile (string fileName);
266 |
267 | [DllImport(SQLITE_DLL, CharSet = CharSet.Unicode, SetLastError = true)]
268 | internal static extern int sqlite3_decompressfile (string fileName);
269 |
270 | [DllImport(SQLITE_DLL)]
271 | internal static extern int sqlite3_key (IntPtr db, byte[] key, int keylen);
272 |
273 | [DllImport(SQLITE_DLL)]
274 | internal static extern int sqlite3_rekey (IntPtr db, byte[] key, int keylen);
275 |
276 | [DllImport(SQLITE_DLL)]
277 | internal static extern int sqlite3_cursor_rowid (IntPtr stmt, int cursor, out long rowid);
278 |
279 | [DllImport(SQLITE_DLL)]
280 | internal static extern int sqlite3_table_cursor (IntPtr stmt, int db, int tableRootPage);
281 |
282 | [DllImport(SQLITE_DLL)]
283 | internal static extern int sqlite3_last_insert_rowid (IntPtr db);
284 | }
285 | }
286 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | *git*-*Subversion* bridge
2 | =========================
3 |
4 | It should be quite obvious to GitHub users why our team likes *git* - we
5 | branch, diff, merge and rebase heavily, work offline, stash, amend commits and
6 | do other *git*-specific things that make *git* so fun and useful.
7 |
8 | However, our corporate standard is *Subversion*. It is simple and
9 | reliable, the history is immutable and the central repository lives in the
10 | secure datacenter. Project managers and salespeople can use *TortoiseSVN* to
11 | access the project repository as well, keeping project documents neatly
12 | organized alongside source repositories; administrators have easy tools for
13 | managing authorization and authentication etc. Everyone is generally happy with
14 | it.
15 |
16 | To reap the best of both worlds, we have setup a *git*-*Subversion*
17 | bridge that synchronizes changes between our team *git* repository and
18 | the corporate *Subversion* repository. The obvious requirement is that
19 | our *git* usage has to be transparent to other *Subversion* users, not
20 | interfere with their work or damage the corporate repository.
21 |
22 | The setup looks like this:
23 |
24 | 
25 |
26 | We have used this setup in production for more than a year (albeit in a
27 | somewhat simpler incarnation), so we have sorted out most of the problem points
28 | and are content with it. It may also work for you in a similar situation.
29 |
30 | This document gives an overview of the setup.
31 |
32 | If you have administrator access to the *Subversion* repository (we
33 | don't), be sure to check out [SubGit](http://subgit.com/). It may or may
34 | not make the setup simpler.
35 |
36 | Overview and caveats
37 | --------------------
38 |
39 | - Each update of *master* in the central *git* repository will trigger
40 | synchronization with *Subversion*. Additionally, there is a *cron* job that
41 | runs the synchronization every *n* minutes (so that the repository is
42 | updated even if there are no commits at *git* side). Concurrent
43 | synchronization is properly guarded with a lock in the bundled
44 | synchronization script.
45 |
46 | - There is no exchange of branches between *git* and *Subversion*. The
47 | *git* side tracks the trunk or any other main *Subversion* branch.
48 | Branching happens at *git* side with short-lived task branches that
49 | need not to be shared with *Subversion* users. Separate *git*
50 | repositories are used for tracking other long-lived *Subversion*
51 | branches.
52 |
53 | - As *Subversion* history is linear, *git* merge commits will be squashed
54 | into a single commit (see examples below). For us this is no problem as,
55 | in the spirit of continuous integration, we consider the task branches to
56 | be lightweight, ephemeral "units of work" that can go to mainline in a
57 | single chunk and as the branch history is retained in *git*.
58 |
59 | - To properly set author information in commits between *git* and
60 | *Subversion*, *Subversion* user passwords need to be available to
61 | the synchronization script. A fairly secure program,
62 | `git-svn-auth-manager`, that keeps passwords in an encrypted
63 | *SQLite* database is bundled and the synchronization script uses
64 | that by default (see description below).
65 |
66 | - *git* history is duplicated as commits first go up to and then come
67 | back down again with a `git-svn-id` from *Subversion*. Although this
68 | may sound confusing it has not been a big problem in practice (see
69 | examples below; note that `--no-ff` is used to record the merges).
70 | *Subversion* history remains clean.
71 |
72 | - Rewriting history on *master* will probably mess up the
73 | *git*-*Subversion* synchronization so it is disabled with the update
74 | hook in the central *git* repository (we haven't tried though, this
75 | just seems a sane precaution).
76 |
77 | - If the project is Windows-only then the *git* bridge repo must be
78 | configured to retain Windows line endings. (*TODO: describe how.*)
79 |
80 | ### Subversion's view on history
81 |
82 | Squashed branch merge commit to *master* from *git* (see `dave.sh` below):
83 |
84 | $ svn log trunk@r9 -l 1
85 | ------------------------------------------------------------------------
86 | r9 | dave | 2012-08-26 13:22:39 +0300 (Sun, 26 Aug 2012) | 10 lines
87 |
88 | 2012-08-26 13:22:36 +0300 | Merge branch 'payment-support'
89 | [Dave]
90 | 2012-08-26 13:22:36 +0300 | Add storage encryption for payments
91 | [Dave]
92 | 2012-08-26 13:22:36 +0300 | Implement credit card payments
93 | [Dave]
94 | 2012-08-26 13:22:36 +0300 | Implement PayPal payments
95 | [Dave]
96 | 2012-08-26 13:22:36 +0300 | Add payment processing interface
97 | [Dave]
98 | ------------------------------------------------------------------------
99 |
100 | Single commit to *master* from *git* (see `carol.sh` below):
101 |
102 | $ svn log trunk@r8 -l 1
103 | ------------------------------------------------------------------------
104 | r8 | carol | 2012-08-26 13:22:36 +0300 (Sun, 26 Aug 2012) | 2 lines
105 |
106 | 2012-08-26 13:22:35 +0300 | Use template filters to represent amounts in localized format
107 | [Carol]
108 | ------------------------------------------------------------------------
109 |
110 | ### Git's view on history
111 |
112 | Single commit before synchronization:
113 |
114 | $ git log a165c -1
115 | commit a165c9857eebb168e44b22278950cd930259394c
116 | Author: Carol
117 | Date: Sun Aug 26 13:22:35 2012 +0300
118 |
119 | Use template filters to represent amounts in localized format
120 |
121 | After synchronization, it will be duplicated with another commit that has come
122 | down from *Subversion*:
123 |
124 | $ git log
125 | ...
126 | commit 10fb01c123851b02f2105c98cb7c9adc47a1bb39
127 | Merge: fc656d9 a165c98
128 | Author: Carol
129 | Date: Sun Aug 26 13:22:36 2012 +0300
130 |
131 | 2012-08-26 13:22:35 +0300 | Use template filters to represent amounts in localized format
132 | [Carol]
133 |
134 | git-svn-id: svn://localhost/trunk@8 49763079-ba47-4a7b-95a0-4af80b88d9d8
135 | ...
136 | commit a165c9857eebb168e44b22278950cd930259394c
137 | Author: Carol
138 | Date: Sun Aug 26 13:22:35 2012 +0300
139 |
140 | Use template filters to represent amounts in localized format
141 | ...
142 |
143 | For each branch merge, an additional squashed merge commit will come down from
144 | *Subversion* as shown in the previous section.
145 |
146 | Setup
147 | -----
148 |
149 | The following walkthrough is provided both for documentation and for
150 | hands-on testing. All of this can be run in one go with the [test
151 | script](http://github.com/mrts/git-svn-bridge/blob/master/test/run-test.sh).
152 | After you have prepared the envrionment, new bridge repositories for other
153 | *Subversion* branches can be set up with the [branch setup script](http://github.com/mrts/git-svn-bridge/blob/master/scripts/setup-svn-branch-git-bridge.sh).
154 |
155 | Start by creating the bridge user (*use your actual email address instead of
156 | YOUREMAIL@gmail.com, it is used later during setup and testing*):
157 |
158 | $ sudo adduser git-svn-bridge
159 | $ sudo su git-svn-bridge
160 | $ set -u
161 | $ YOUR_EMAIL=YOUREMAIL@gmail.com
162 | $ git config --global user.name "Git-SVN Bridge (GIT SIDE)"
163 | $ git config --global user.email "$YOUR_EMAIL"
164 | $ cd
165 | $ mkdir {bin,git,svn,test}
166 |
167 | ### Subversion
168 |
169 | Assure that *Subversion* caches passwords (*only last git-svn-auth-manager
170 | reset user password will be cached; let me know if this does not meet your
171 | security requirements, there are ways around this*):
172 |
173 | $ echo 'store-plaintext-passwords = yes' >> ~/.subversion/servers
174 |
175 | Create the *Subversion* repository (*in real life you would simply use the
176 | existing central Subversion repository*):
177 |
178 | $ cd ~/svn
179 | $ svnadmin create svn-repo
180 | $ svn co file://`pwd`/svn-repo svn-checkout
181 | Checked out revision 0.
182 |
183 | Commit a test revision to *Subversion*:
184 |
185 | $ cd svn-checkout
186 | $ mkdir -p trunk/src
187 | $ echo 'int main() { return 0; }' > trunk/src/main.cpp
188 | $ svn add trunk
189 | A trunk
190 | A trunk/src
191 | A trunk/src/main.cpp
192 | $ svn ci -m "First commit."
193 | Adding trunk
194 | Adding trunk/src
195 | Adding trunk/src/main.cpp
196 | Transmitting file data .
197 | Committed revision 1.
198 |
199 | Setup `svnserve` to serve the repository:
200 |
201 | $ cd ~/svn
202 |
203 | $ SVNSERVE_PIDFILE="$HOME/svn/svnserve.pid"
204 | $ SVNSERVE_LOGFILE="$HOME/svn/svnserve.log"
205 | $ SVNSERVE_CONFFILE="$HOME/svn/svnserve.conf"
206 | $ SVNSERVE_USERSFILE="$HOME/svn/svnserve.users"
207 |
208 | $ >> $SVNSERVE_LOGFILE
209 |
210 | $ cat > "$SVNSERVE_CONFFILE" << EOT
211 | [general]
212 | realm = git-SVN test
213 | anon-access = none
214 | password-db = $SVNSERVE_USERSFILE
215 | EOT
216 |
217 | $ cat > "$SVNSERVE_USERSFILE" << EOT
218 | [users]
219 | git-svn-bridge = git-svn-bridge
220 | alice = alice
221 | bob = bob
222 | carol = carol
223 | dave = dave
224 | EOT
225 |
226 | $ TAB="`printf '\t'`"
227 |
228 | $ cat > ~/svn/Makefile << EOT
229 | svnserve-start:
230 | ${TAB}svnserve -d \\
231 | ${TAB}${TAB}--pid-file "$SVNSERVE_PIDFILE" \\
232 | ${TAB}${TAB}--log-file "$SVNSERVE_LOGFILE" \\
233 | ${TAB}${TAB}--config-file "$SVNSERVE_CONFFILE" \\
234 | ${TAB}${TAB}-r ~/svn/svn-repo
235 |
236 | svnserve-stop:
237 | ${TAB}kill \`cat "$SVNSERVE_PIDFILE"\`
238 | EOT
239 |
240 | Start `svnserve`:
241 |
242 | $ make svnserve-start
243 |
244 | ### Git
245 |
246 | Setup the central repository that *git* users will use:
247 |
248 | $ cd ~/git
249 | $ git init --bare git-central-repo-trunk.git
250 | Initialized empty Git repository in /home/git-svn-bridge/git/git-central-repo-trunk.git/
251 | $ cd git-central-repo-trunk.git
252 | $ git remote add svn-bridge ../git-svn-bridge-trunk
253 |
254 | Setup the *git*-*Subversion* bridge repository:
255 |
256 | $ cd ~/git
257 | $ SVN_REPO_URL="svn://localhost/trunk"
258 | $ git svn init --prefix=svn/ $SVN_REPO_URL git-svn-bridge-trunk
259 | Initialized empty Git repository in /home/git-svn-bridge/git/git-svn-bridge-trunk/.git/
260 |
261 | Fetch changes from *Subversion*:
262 |
263 | $ cd git-svn-bridge-trunk
264 | $ AUTHORS='/tmp/git-svn-bridge-authors'
265 | $ echo "git-svn-bridge = Git SVN Bridge <${YOUR_EMAIL}>" > $AUTHORS
266 | $ git svn fetch --authors-file="$AUTHORS" --log-window-size 10000
267 | Authentication realm: git-SVN test
268 | Password for 'git-svn-bridge': git-svn-bridge
269 | A src/main.cpp
270 | r1 = 061725282bdccf7f4a8efa66ee34b195ca7070fc (refs/remotes/svn/git-svn)
271 | Checked out HEAD:
272 | file:///home/git-svn-bridge/svn/svn-repo/trunk r1
273 |
274 | Verify that the result is OK:
275 |
276 | $ git branch -a -v
277 | * master 0617252 First commit.
278 | remotes/svn/git-svn 0617252 First commit.
279 |
280 | Add the central repository as a remote to the bridge repository and push
281 | changes from *Subversion* to the central repository:
282 |
283 | $ git remote add git-central-repo ../git-central-repo-trunk.git
284 | $ git push --all git-central-repo
285 | Counting objects: 4, done.
286 | Writing objects: 100% (4/4), 332 bytes, done.
287 | Total 4 (delta 0), reused 0 (delta 0)
288 | Unpacking objects: 100% (4/4), done.
289 | To ../git-central-repo-trunk.git
290 | * [new branch] master -> master
291 |
292 | Clone the central repository and verify that the *Subversion* test
293 | commit is there:
294 |
295 | $ cd ~/git
296 | $ git clone git-central-repo-trunk.git git-central-repo-clone
297 | Cloning into 'git-central-repo-clone'...
298 | done.
299 |
300 | $ cd git-central-repo-clone
301 | $ git log
302 | commit 061725282bdccf7f4a8efa66ee34b195ca7070fc
303 | Author: git-svn-bridge
304 | Date: Wed Aug 15 11:38:57 2012 +0000
305 |
306 | First commit.
307 |
308 | git-svn-id: file:///home/git-svn-bridge/svn/svn-repo/trunk@1 b4f7b086-5416-...
309 |
310 | Create the *git* hook that blocks non-fast-forward commits in the
311 | central repository:
312 |
313 | $ cd ~/git/git-central-repo-trunk.git
314 | $ cat > hooks/update << 'EOT'
315 | #!/bin/bash
316 | set -u
317 | refname=$1
318 | shaold=$2
319 | shanew=$3
320 |
321 | # we are only interested in commits to master
322 | [[ "$refname" = "refs/heads/master" ]] || exit 0
323 |
324 | # don't allow non-fast-forward commits
325 | if [[ $(git merge-base "$shanew" "$shaold") != "$shaold" ]]; then
326 | echo "Non-fast-forward commits to master are not allowed"
327 | exit 1
328 | fi
329 | EOT
330 |
331 | $ chmod 755 hooks/update
332 |
333 | Create the *git* hook that triggers synchronization:
334 |
335 | $ cat > hooks/post-update << 'EOT'
336 | #!/bin/bash
337 |
338 | # trigger synchronization only on commit to master
339 | for arg in "$@"; do
340 | if [[ "$arg" = "refs/heads/master" ]]; then
341 | /home/git-svn-bridge/bin/synchronize-git-svn.sh GIT_HOOK
342 | exit $?
343 | fi
344 | done
345 | EOT
346 |
347 | $ chmod 755 hooks/post-update
348 |
349 | $ cat > ~/bin/synchronize-git-svn.sh << 'EOT'
350 | # test script to verify that the git hook works properly
351 | echo "Commit from $1 to master" > /tmp/test-synchronize-git-svn
352 | exit 1 # test that error exit does not abort the update
353 | EOT
354 |
355 | $ chmod 755 ~/bin/synchronize-git-svn.sh
356 |
357 | Test that the hook works:
358 |
359 | $ cd ~/git/git-central-repo-clone
360 | $ echo "void do_nothing() { }" >> src/main.cpp
361 |
362 | $ git commit -am "Update main.cpp"
363 | [master 2c833e2] Update main.cpp
364 | 1 file changed, 1 insertion(+)
365 |
366 | $ git push
367 | Counting objects: 7, done.
368 | Writing objects: 100% (4/4), 341 bytes, done.
369 | Total 4 (delta 0), reused 0 (delta 0)
370 | Unpacking objects: 100% (4/4), done.
371 | To /home/git-svn-bridge/git/git-central-repo-trunk.git
372 | 5b73892..2c833e2 master -> master
373 |
374 | $ cat /tmp/test-synchronize-git-svn
375 | Commit from GIT_HOOK to master
376 |
377 | Verify that non-fast-forward commits to *master* are not allowed:
378 |
379 | $ echo "void do_nothing() { }" >> src/main.cpp
380 | $ git add src/
381 | $ git commit --amend
382 | [master d2f9a16] Update main.cpp
383 | 1 file changed, 2 insertions(+)
384 |
385 | $ git push --force
386 | Counting objects: 7, done.
387 | Compressing objects: 100% (2/2), done.
388 | Writing objects: 100% (4/4), 345 bytes, done.
389 | Total 4 (delta 0), reused 0 (delta 0)
390 | Unpacking objects: 100% (4/4), done.
391 | remote: Non-fast-forward commits to master are not allowed
392 | remote: error: hook declined to update refs/heads/master
393 | To /home/git-svn-bridge/git/git-central-repo-trunk.git
394 | ! [remote rejected] master -> master (hook declined)
395 | error: failed to push some refs to '/home/git-svn-bridge/git/git-central-repo-trunk.git'
396 |
397 | $ git reset --hard origin/master
398 |
399 | So far, so good. Let's wire in the real synchronization utilities now.
400 |
401 | ### Synchronization utilities
402 |
403 | Real synchronization relies on
404 |
405 | - the [synchronization
406 | script](https://github.com/mrts/git-svn-bridge/blob/master/scripts/synchronize-git-svn.sh)
407 | that controls the actual synchronization
408 |
409 | - `git-svn-auth-manager`, a utility that manages *Subversion*
410 | authentication and commit author mapping between *git* and
411 | *Subversion* (**note that this is the sweet spot of the solution**);
412 | it is described in more detail in a [separate
413 | README](https://github.com/mrts/git-svn-bridge/blob/master/git-svn-auth-manager/README.rst).
414 |
415 | Start by cloning this repository:
416 |
417 | $ cd ~/git
418 | $ git clone --recursive git://github.com/mrts/git-svn-bridge.git github-git-svn-bridge-utils
419 |
420 | |**Warning to Ubuntu 16.04 users**|
421 | |---------------------------------|
422 | |The versions of *Mono* and *Git* provided in Ubuntu 16.04 cause problems as described below, please use [latest *Mono*](http://www.mono-project.com/docs/getting-started/install/linux/#debian-ubuntu-and-derivatives) and [*Git*](https://launchpad.net/~git-core/+archive/ubuntu/ppa) if you run into problems.|
423 |
424 | #### git-svn-auth-manager
425 |
426 | Install required libraries and tools:
427 |
428 | $ sudo apt-get install build-essential mono-devel libssl-dev tcl
429 |
430 | Change the encryption key:
431 |
432 | $ cd github-git-svn-bridge-utils/git-svn-auth-manager
433 | $ ENCRYPTION_KEY=`tr -dc '[:alnum:]' < /dev/urandom | head -c 16`
434 | $ sed -i "s/CHANGETHIS/$ENCRYPTION_KEY/" src/EncryptedUserRepository.cs
435 | $ git diff src
436 | ...
437 | - private const string ENCRYPTION_KEY = "CHANGETHIS";
438 | + private const string ENCRYPTION_KEY = "TNwwmT2Wc3xVTole";
439 | ...
440 |
441 | This is generally not necessary, but if you have an old database lying
442 | around from previous runs, it should be removed now as the encryption
443 | key has changed (**careful with your actual user information**):
444 |
445 | $ make mrproper
446 | ...
447 | rm -f ~/.config/git-svn-auth-manager/userinfo.db
448 | ...
449 |
450 | Build and install `git-svn-auth-manager`:
451 |
452 | $ make install
453 | ...
454 | install -m 711 -D bin/git-svn-auth-manager ~/bin/git-svn-auth-manager
455 |
456 | Verify that it works:
457 |
458 | $ ~/bin/git-svn-auth-manager
459 | git-svn-auth-manager: too few, too many or invalid arguments
460 |
461 | Helper utility for running a git-SVN bridge.
462 | Manages SVN authentication for git and user mapping between git and SVN.
463 |
464 | Usage:
465 | either with a single non-option argument to output user
466 | name and email suitable for `git --authors-prog`:
467 |
468 | git-svn-auth-manager SVN_USERNAME
469 |
470 | or with a single option to add users or change passwords:
471 |
472 | git-svn-auth-manager OPTION=SVN_USERNAME
473 |
474 | or with a single option and single non-option argument to reset
475 | SVN authentication cache:
476 |
477 | git-svn-auth-manager --reset-auth-for=EMAIL SVN_URL
478 |
479 | Options:
480 | --help, -h Show help
481 | --add-user, -a=VALUE Add user information to the database
482 | --change-passwd-for, -p=VALUE
483 | Change user's password in the database
484 | --reset-auth-for, -r=VALUE
485 | Reset SVN auth cache with user's credentials;
486 | option argument is user's email; SVN URL
487 | required as non-option argument
488 |
489 | |**Note**|
490 | |--------|
491 | |If `~/bin/git-svn-auth-manager` crashes, then this is caused by *Mono* problems, please update *Mono* as described above|
492 |
493 | Secure the key - as encryption key is embedded in
494 | `git-svn-auth-manager`, it needs to be owned by root and be made
495 | execute-only (`make install` *took care of the execute-only part already,
496 | but let's be extra safe and explicit here*):
497 |
498 | $ sudo chown root: ~/bin/git-svn-auth-manager
499 | $ sudo chmod 711 ~/bin/git-svn-auth-manager
500 | $ ls -l ~/bin/git-svn-auth-manager
501 | -rwx--x--x 1 root root 697208 Aug 23 17:38 git-svn-auth-manager
502 |
503 | Add the *git-svn-bridge* user for testing (*as before, use your actual email
504 | address instead of YOUREMAIL@gmail.com and 'git-svn-bridge' as password*):
505 |
506 | $ ~/bin/git-svn-auth-manager -a git-svn-bridge
507 | Adding/overwriting SVN user git-svn-bridge
508 | SVN password: git-svn-bridge
509 | SVN password (confirm): git-svn-bridge
510 | Email: YOUREMAIL@gmail.com
511 | Name: Git-SVN Bridge
512 |
513 | Verify that the database is really encrypted:
514 |
515 | $ echo .dump | sqlite3 ~/.config/git-svn-auth-manager/userinfo.db
516 | PRAGMA foreign_keys=OFF;
517 | BEGIN TRANSACTION;
518 | /**** ERROR: (26) file is encrypted or is not a database *****/
519 | ROLLBACK; -- due to errors
520 |
521 | Create configuration files and enable email notifications to users for
522 | *Subversion* authentication failures (*substitute YOURGMAILPASSWORD with
523 | real GMail password, the credentials will be used to authenticate
524 | GMail SMTP connections*):
525 |
526 | $ make install_config
527 | ...
528 | install -m 600 -D config-just-enough ~/.config/git-svn-auth-manager/config
529 | $ GITSVNAUTHMGRCONF="$HOME/.config/git-svn-auth-manager/config"
530 | $ sed -i "s/username@gmail.com/${YOUR_EMAIL}/" "$GITSVNAUTHMGRCONF"
531 | $ sed -i 's/userpassword/YOURGMAILPASSWORD/' "$GITSVNAUTHMGRCONF"
532 |
533 | $ cat "$GITSVNAUTHMGRCONF"
534 |
535 |
536 |
537 |
538 |
539 |
540 |
541 |
542 |
543 | Test that email sending works (the invalid SVN repository URL triggers
544 | an error that will cause the email to be sent):
545 |
546 | $ ~/bin/git-svn-auth-manager -r ${YOUR_EMAIL} non-existing-path
547 | git-svn-auth-manager: error email sent
548 | git-svn-auth-manager: System.ApplicationException: Error executing `svn info --username "git-svn-bridge" --password "*****" "non-existing-path"`:
549 | svn: 'non-existing-path' is not a working copy
550 |
551 | Verify that the error email arrives to your mailbox. It should look like [this
552 | sample](https://github.com/mrts/git-svn-bridge/blob/master/git-svn-auth-manager/mail-sample.txt).
553 |
554 | #### synchronize-git-svn.sh
555 |
556 | Start by copying the script and sample configuration to `~/bin`:
557 |
558 | $ cd ~/bin
559 | $ cp ../git/github-git-svn-bridge-utils/scripts/synchronize-git-svn.sh .
560 | $ cp ../git/github-git-svn-bridge-utils/scripts/synchronize-git-svn.sh.config .
561 |
562 | And test it all:
563 |
564 | $ ./synchronize-git-svn.sh
565 |
566 | $ cd ~/git/git-central-repo-clone
567 | $ git pull --rebase
568 | $ echo "void more_do_nothing() { }" >> src/main.cpp
569 | $ git commit -am "Add more_do_nothing() to main.cpp"
570 | [master 0c6e72a] Add more_do_nothing() to main.cpp
571 | 1 file changed, 1 insertion(+)
572 | $ git push
573 | Counting objects: 7, done.
574 | Compressing objects: 100% (2/2), done.
575 | Writing objects: 100% (4/4), 374 bytes, done.
576 | Total 4 (delta 0), reused 0 (delta 0)
577 | Unpacking objects: 100% (4/4), done.
578 | To /home/git-svn-bridge/git/git-central-repo-trunk.git
579 | 001c5c9..0c6e72a master -> master
580 |
581 | |**Note**|
582 | |--------|
583 | |If you see *Not a git repository* errors during push, then this is caused by problems with some versions of *Git*, please update *Git* as described above|
584 |
585 | We are done with the setup now and will proceed with semi-realistic
586 | virtual developer testing in the next section.
587 |
588 | Test synchronization
589 | --------------------
590 |
591 | The scenario:
592 |
593 | - Alice commits to *trunk* in *Subversion*
594 |
595 | - Bob commits to *trunk* in *Subversion*
596 |
597 | - Carol commits a number of changes directly to *master* and pushes in *git*
598 | (triggers synchronization with the update hook)
599 |
600 | - Dave works on the task branch *payment-support*, merges it to *master* and
601 | pushes in *git* (triggers synchronization with the update hook)
602 |
603 | - Finally, *cron* triggers synchronization explicitly.
604 |
605 | Let's setup the stage:
606 |
607 | $ cd ~/test
608 | $ mkdir {alice,bob,carol,dave}
609 |
610 | $ for name in alice bob; do
611 | echo "Use '$name' as password and '$name@company.com' as email"
612 | ~/bin/git-svn-auth-manager -a $name
613 | pushd $name
614 | svn --username $name --password $name co $SVN_REPO_URL
615 | popd
616 | done
617 |
618 | $ for name in carol dave; do
619 | echo "Use '$name' as password and '$name@company.com' as email"
620 | ~/bin/git-svn-auth-manager -a $name
621 | pushd $name
622 | git clone ~/git/git-central-repo-trunk.git git-trunk
623 | cd git-trunk
624 | git config user.name `~/bin/git-svn-auth-manager $name | sed 's/ <.*//'`
625 | git config user.email `~/bin/git-svn-auth-manager $name | sed 's/.*<\(.*\)>/\1/'`
626 | popd
627 | done
628 |
629 | $ cat > alice.sh << 'EOT'
630 | #!/bin/bash
631 | pushd alice/trunk
632 | echo 'void alice() { }' >> src/alice.cpp
633 | svn --username alice --password alice up
634 | svn add src/alice.cpp
635 | svn --username alice --password alice ci -m "Protect the global cache with a mutex"
636 | popd
637 | EOT
638 |
639 | $ cat > bob.sh << 'EOT'
640 | #!/bin/bash
641 | pushd bob/trunk
642 | echo 'void bob() { }' >> src/bob.cpp
643 | svn --username bob --password bob up
644 | svn add src/bob.cpp
645 | svn --username bob --password bob ci -m "Cache rendered templates"
646 | echo 'void bob2() { }' >> src/bob.cpp
647 | svn --username bob --password bob up
648 | svn --username bob --password bob ci -m "Add tags to articles"
649 | popd
650 | EOT
651 |
652 | $ cat > carol.sh << 'EOT'
653 | #!/bin/bash
654 | pushd carol/git-trunk
655 | echo 'void carol1() { }' >> src/carol.cpp
656 | git add src/carol.cpp
657 | git commit -m "Add template tag library"
658 | echo 'void carol2() { }' >> src/carol.cpp
659 | git commit -am "Use template tag library for localized date format"
660 | git pull --rebase
661 | git push
662 | echo 'void carol3() { }' >> src/carol.cpp
663 | git commit -am "Use template filters to represent amounts in localized format"
664 | git pull --rebase
665 | git push
666 | popd
667 | EOT
668 |
669 | $ cat > dave.sh << 'EOT'
670 | #!/bin/bash
671 | # dave is working on a task branch
672 | pushd dave/git-trunk
673 | git checkout -b payment-support
674 | echo 'void dave1() { }' >> src/dave.cpp
675 | git add src/dave.cpp
676 | git commit -m "Add payment processing interface"
677 | echo 'void dave2() { }' >> src/dave.cpp
678 | git commit -am "Implement PayPal payments"
679 | echo 'void dave3() { }' >> src/dave.cpp
680 | git commit -am "Implement credit card payments"
681 | git fetch
682 | git rebase origin/master
683 | echo 'void dave4() { }' >> src/dave.cpp
684 | git commit -am "Add storage encryption for payments"
685 | git checkout master
686 | git pull --rebase
687 | git merge --no-ff payment-support
688 | git push
689 | popd
690 | EOT
691 |
692 | $ cat > cron.sh << 'EOT'
693 | #!/bin/bash
694 | ~/bin/synchronize-git-svn.sh CRON
695 | EOT
696 |
697 | $ chmod 755 *.sh
698 |
699 | $ cat > Makefile << 'EOT'
700 | all: alice bob carol dave cron
701 | .PHONY: all alice bob carol dave cron
702 |
703 | EOT
704 |
705 | $ for name in alice bob carol dave cron; do
706 | echo -e "${name}:\n\t./${name}.sh\n" >> Makefile
707 | done
708 |
709 | And now we let our imaginary developers loose to the source control land:
710 |
711 | make
712 |
713 | Or, to test that mutual exclusion works, run the scripts in parallel:
714 |
715 | make -j 5
716 |
717 | Finally, shut down ``svnserve``:
718 |
719 | make -f ~/svn/Makefile svnserve-stop
720 |
721 | Verify that all went well (you should see clean history according to the
722 | examples in the *Overview* section):
723 |
724 | $ cd ~/svn/svn-checkout
725 | $ svn up
726 | $ svn log
727 |
728 | $ cd ~/git/git-central-repo-clone
729 | $ git pull --rebase
730 | $ git log
731 |
732 | Test setting up repos for other branches
733 | ----------------------------------------
734 |
735 | $ make -f ~/svn/Makefile svnserve-start
736 |
737 | $ cd ~/svn/svn-checkout
738 | $ svn mkdir branches
739 | $ svn cp trunk branches/1.x
740 | $ svn ci -m "Branch trunk to 1.x"
741 |
742 | $ cd ~/git
743 | $ ./github-git-svn-bridge-utils/scripts/setup-svn-branch-git-bridge.sh
744 | $ git clone central-repo-1.x.git central-repo-1.x-clone
745 | $ cd central-repo-1.x-clone
746 | $ git log
747 | commit 095e7a01f102f79224df4283a67c4624986679a1
748 | Author: git-svn-bridge@company.com
749 | Date: Sun Aug 26 18:38:34 2012 +0000
750 |
751 | Branch trunk to 1.x
752 |
753 | git-svn-id: svn://localhost/branches/1.x@10 1db59d55-421c-46dd...
754 |
755 | $ make -f ~/svn/Makefile svnserve-stop
756 |
--------------------------------------------------------------------------------
/git-svn-auth-manager/src/Options.cs:
--------------------------------------------------------------------------------
1 | //
2 | // Options.cs
3 | //
4 | // Authors:
5 | // Jonathan Pryor
6 | // Federico Di Gregorio
7 | //
8 | // Copyright (C) 2008 Novell (http://www.novell.com)
9 | // Copyright (C) 2009 Federico Di Gregorio.
10 | //
11 | // Permission is hereby granted, free of charge, to any person obtaining
12 | // a copy of this software and associated documentation files (the
13 | // "Software"), to deal in the Software without restriction, including
14 | // without limitation the rights to use, copy, modify, merge, publish,
15 | // distribute, sublicense, and/or sell copies of the Software, and to
16 | // permit persons to whom the Software is furnished to do so, subject to
17 | // the following conditions:
18 | //
19 | // The above copyright notice and this permission notice shall be
20 | // included in all copies or substantial portions of the Software.
21 | //
22 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
23 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
24 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
25 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
26 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
27 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
28 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
29 | //
30 |
31 | // Compile With:
32 | // gmcs -debug+ -r:System.Core Options.cs -o:NDesk.Options.dll
33 | // gmcs -debug+ -d:LINQ -r:System.Core Options.cs -o:NDesk.Options.dll
34 | //
35 | // The LINQ version just changes the implementation of
36 | // OptionSet.Parse(IEnumerable), and confers no semantic changes.
37 |
38 | //
39 | // A Getopt::Long-inspired option parsing library for C#.
40 | //
41 | // NDesk.Options.OptionSet is built upon a key/value table, where the
42 | // key is a option format string and the value is a delegate that is
43 | // invoked when the format string is matched.
44 | //
45 | // Option format strings:
46 | // Regex-like BNF Grammar:
47 | // name: .+
48 | // type: [=:]
49 | // sep: ( [^{}]+ | '{' .+ '}' )?
50 | // aliases: ( name type sep ) ( '|' name type sep )*
51 | //
52 | // Each '|'-delimited name is an alias for the associated action. If the
53 | // format string ends in a '=', it has a required value. If the format
54 | // string ends in a ':', it has an optional value. If neither '=' or ':'
55 | // is present, no value is supported. `=' or `:' need only be defined on one
56 | // alias, but if they are provided on more than one they must be consistent.
57 | //
58 | // Each alias portion may also end with a "key/value separator", which is used
59 | // to split option values if the option accepts > 1 value. If not specified,
60 | // it defaults to '=' and ':'. If specified, it can be any character except
61 | // '{' and '}' OR the *string* between '{' and '}'. If no separator should be
62 | // used (i.e. the separate values should be distinct arguments), then "{}"
63 | // should be used as the separator.
64 | //
65 | // Options are extracted either from the current option by looking for
66 | // the option name followed by an '=' or ':', or is taken from the
67 | // following option IFF:
68 | // - The current option does not contain a '=' or a ':'
69 | // - The current option requires a value (i.e. not a Option type of ':')
70 | //
71 | // The `name' used in the option format string does NOT include any leading
72 | // option indicator, such as '-', '--', or '/'. All three of these are
73 | // permitted/required on any named option.
74 | //
75 | // Option bundling is permitted so long as:
76 | // - '-' is used to start the option group
77 | // - all of the bundled options are a single character
78 | // - at most one of the bundled options accepts a value, and the value
79 | // provided starts from the next character to the end of the string.
80 | //
81 | // This allows specifying '-a -b -c' as '-abc', and specifying '-D name=value'
82 | // as '-Dname=value'.
83 | //
84 | // Option processing is disabled by specifying "--". All options after "--"
85 | // are returned by OptionSet.Parse() unchanged and unprocessed.
86 | //
87 | // Unprocessed options are returned from OptionSet.Parse().
88 | //
89 | // Examples:
90 | // int verbose = 0;
91 | // OptionSet p = new OptionSet ()
92 | // .Add ("v", v => ++verbose)
93 | // .Add ("name=|value=", v => Console.WriteLine (v));
94 | // p.Parse (new string[]{"-v", "--v", "/v", "-name=A", "/name", "B", "extra"});
95 | //
96 | // The above would parse the argument string array, and would invoke the
97 | // lambda expression three times, setting `verbose' to 3 when complete.
98 | // It would also print out "A" and "B" to standard output.
99 | // The returned array would contain the string "extra".
100 | //
101 | // C# 3.0 collection initializers are supported and encouraged:
102 | // var p = new OptionSet () {
103 | // { "h|?|help", v => ShowHelp () },
104 | // };
105 | //
106 | // System.ComponentModel.TypeConverter is also supported, allowing the use of
107 | // custom data types in the callback type; TypeConverter.ConvertFromString()
108 | // is used to convert the value option to an instance of the specified
109 | // type:
110 | //
111 | // var p = new OptionSet () {
112 | // { "foo=", (Foo f) => Console.WriteLine (f.ToString ()) },
113 | // };
114 | //
115 | // Random other tidbits:
116 | // - Boolean options (those w/o '=' or ':' in the option format string)
117 | // are explicitly enabled if they are followed with '+', and explicitly
118 | // disabled if they are followed with '-':
119 | // string a = null;
120 | // var p = new OptionSet () {
121 | // { "a", s => a = s },
122 | // };
123 | // p.Parse (new string[]{"-a"}); // sets v != null
124 | // p.Parse (new string[]{"-a+"}); // sets v != null
125 | // p.Parse (new string[]{"-a-"}); // sets v == null
126 | //
127 | using System;
128 | using System.Collections;
129 | using System.Collections.Generic;
130 | using System.Collections.ObjectModel;
131 | using System.ComponentModel;
132 | using System.Globalization;
133 | using System.IO;
134 | using System.Runtime.Serialization;
135 | using System.Security.Permissions;
136 | using System.Text;
137 | using System.Text.RegularExpressions;
138 |
139 | #if LINQ
140 | using System.Linq;
141 | #endif
142 |
143 | namespace Mono.Options
144 | {
145 | static class StringCoda
146 | {
147 |
148 | public static IEnumerable WrappedLines (string self, params int[] widths)
149 | {
150 | IEnumerable w = widths;
151 | return WrappedLines (self, w);
152 | }
153 |
154 | public static IEnumerable WrappedLines (string self, IEnumerable widths)
155 | {
156 | if (widths == null)
157 | throw new ArgumentNullException ("widths");
158 | return CreateWrappedLinesIterator (self, widths);
159 | }
160 |
161 | private static IEnumerable CreateWrappedLinesIterator (string self, IEnumerable widths)
162 | {
163 | if (string.IsNullOrEmpty (self)) {
164 | yield return string.Empty;
165 | yield break;
166 | }
167 | using (IEnumerator ewidths = widths.GetEnumerator ()) {
168 | bool? hw = null;
169 | int width = GetNextWidth (ewidths, int.MaxValue, ref hw);
170 | int start = 0, end;
171 | do {
172 | end = GetLineEnd (start, width, self);
173 | char c = self [end - 1];
174 | if (char.IsWhiteSpace (c))
175 | --end;
176 | bool needContinuation = end != self.Length && !IsEolChar (c);
177 | string continuation = "";
178 | if (needContinuation) {
179 | --end;
180 | continuation = "-";
181 | }
182 | string line = self.Substring (start, end - start) + continuation;
183 | yield return line;
184 | start = end;
185 | if (char.IsWhiteSpace (c))
186 | ++start;
187 | width = GetNextWidth (ewidths, width, ref hw);
188 | } while (end < self.Length);
189 | }
190 | }
191 |
192 | private static int GetNextWidth (IEnumerator ewidths, int curWidth, ref bool? eValid)
193 | {
194 | if (!eValid.HasValue || (eValid.HasValue && eValid.Value)) {
195 | curWidth = (eValid = ewidths.MoveNext ()).Value ? ewidths.Current : curWidth;
196 | // '.' is any character, - is for a continuation
197 | const string minWidth = ".-";
198 | if (curWidth < minWidth.Length)
199 | throw new ArgumentOutOfRangeException ("widths",
200 | string.Format ("Element must be >= {0}, was {1}.", minWidth.Length, curWidth));
201 | return curWidth;
202 | }
203 | // no more elements, use the last element.
204 | return curWidth;
205 | }
206 |
207 | private static bool IsEolChar (char c)
208 | {
209 | return !char.IsLetterOrDigit (c);
210 | }
211 |
212 | private static int GetLineEnd (int start, int length, string description)
213 | {
214 | int end = System.Math.Min (start + length, description.Length);
215 | int sep = -1;
216 | for (int i = start; i < end; ++i) {
217 | if (description [i] == '\n')
218 | return i + 1;
219 | if (IsEolChar (description [i]))
220 | sep = i + 1;
221 | }
222 | if (sep == -1 || end == description.Length)
223 | return end;
224 | return sep;
225 | }
226 | }
227 |
228 | public class OptionValueCollection : IList, IList
229 | {
230 |
231 | List values = new List ();
232 | OptionContext c;
233 |
234 | internal OptionValueCollection (OptionContext c)
235 | {
236 | this.c = c;
237 | }
238 |
239 | #region ICollection
240 | void ICollection.CopyTo (Array array, int index)
241 | {
242 | (values as ICollection).CopyTo (array, index);
243 | }
244 |
245 | bool ICollection.IsSynchronized { get { return (values as ICollection).IsSynchronized; } }
246 |
247 | object ICollection.SyncRoot { get { return (values as ICollection).SyncRoot; } }
248 | #endregion
249 |
250 | #region ICollection
251 | public void Add (string item)
252 | {
253 | values.Add (item);
254 | }
255 |
256 | public void Clear ()
257 | {
258 | values.Clear ();
259 | }
260 |
261 | public bool Contains (string item)
262 | {
263 | return values.Contains (item);
264 | }
265 |
266 | public void CopyTo (string[] array, int arrayIndex)
267 | {
268 | values.CopyTo (array, arrayIndex);
269 | }
270 |
271 | public bool Remove (string item)
272 | {
273 | return values.Remove (item);
274 | }
275 |
276 | public int Count { get { return values.Count; } }
277 |
278 | public bool IsReadOnly { get { return false; } }
279 | #endregion
280 |
281 | #region IEnumerable
282 | IEnumerator IEnumerable.GetEnumerator ()
283 | {
284 | return values.GetEnumerator ();
285 | }
286 | #endregion
287 |
288 | #region IEnumerable
289 | public IEnumerator GetEnumerator ()
290 | {
291 | return values.GetEnumerator ();
292 | }
293 | #endregion
294 |
295 | #region IList
296 | int IList.Add (object value)
297 | {
298 | return (values as IList).Add (value);
299 | }
300 |
301 | bool IList.Contains (object value)
302 | {
303 | return (values as IList).Contains (value);
304 | }
305 |
306 | int IList.IndexOf (object value)
307 | {
308 | return (values as IList).IndexOf (value);
309 | }
310 |
311 | void IList.Insert (int index, object value)
312 | {
313 | (values as IList).Insert (index, value);
314 | }
315 |
316 | void IList.Remove (object value)
317 | {
318 | (values as IList).Remove (value);
319 | }
320 |
321 | void IList.RemoveAt (int index)
322 | {
323 | (values as IList).RemoveAt (index);
324 | }
325 |
326 | bool IList.IsFixedSize { get { return false; } }
327 |
328 | object IList.this [int index] {get { return this [index];} set { (values as IList) [index] = value;}
329 | }
330 | #endregion
331 |
332 | #region IList
333 | public int IndexOf (string item)
334 | {
335 | return values.IndexOf (item);
336 | }
337 |
338 | public void Insert (int index, string item)
339 | {
340 | values.Insert (index, item);
341 | }
342 |
343 | public void RemoveAt (int index)
344 | {
345 | values.RemoveAt (index);
346 | }
347 |
348 | private void AssertValid (int index)
349 | {
350 | if (c.Option == null)
351 | throw new InvalidOperationException ("OptionContext.Option is null.");
352 | if (index >= c.Option.MaxValueCount)
353 | throw new ArgumentOutOfRangeException ("index");
354 | if (c.Option.OptionValueType == OptionValueType.Required &&
355 | index >= values.Count)
356 | throw new OptionException (string.Format (
357 | c.OptionSet.MessageLocalizer ("Missing required value for option '{0}'."), c.OptionName),
358 | c.OptionName);
359 | }
360 |
361 | public string this [int index] {
362 | get {
363 | AssertValid (index);
364 | return index >= values.Count ? null : values [index];
365 | }
366 | set {
367 | values [index] = value;
368 | }
369 | }
370 | #endregion
371 |
372 | public List ToList ()
373 | {
374 | return new List (values);
375 | }
376 |
377 | public string[] ToArray ()
378 | {
379 | return values.ToArray ();
380 | }
381 |
382 | public override string ToString ()
383 | {
384 | return string.Join (", ", values.ToArray ());
385 | }
386 | }
387 |
388 | public class OptionContext
389 | {
390 | private Option option;
391 | private string name;
392 | private int index;
393 | private OptionSet set;
394 | private OptionValueCollection c;
395 |
396 | public OptionContext (OptionSet set)
397 | {
398 | this.set = set;
399 | this.c = new OptionValueCollection (this);
400 | }
401 |
402 | public Option Option {
403 | get { return option;}
404 | set { option = value;}
405 | }
406 |
407 | public string OptionName {
408 | get { return name;}
409 | set { name = value;}
410 | }
411 |
412 | public int OptionIndex {
413 | get { return index;}
414 | set { index = value;}
415 | }
416 |
417 | public OptionSet OptionSet {
418 | get { return set;}
419 | }
420 |
421 | public OptionValueCollection OptionValues {
422 | get { return c;}
423 | }
424 | }
425 |
426 | public enum OptionValueType
427 | {
428 | None,
429 | Optional,
430 | Required,
431 | }
432 |
433 | public abstract class Option
434 | {
435 | string prototype, description;
436 | string[] names;
437 | OptionValueType type;
438 | int count;
439 | string[] separators;
440 |
441 | protected Option (string prototype, string description)
442 | : this (prototype, description, 1)
443 | {
444 | }
445 |
446 | protected Option (string prototype, string description, int maxValueCount)
447 | {
448 | if (prototype == null)
449 | throw new ArgumentNullException ("prototype");
450 | if (prototype.Length == 0)
451 | throw new ArgumentException ("Cannot be the empty string.", "prototype");
452 | if (maxValueCount < 0)
453 | throw new ArgumentOutOfRangeException ("maxValueCount");
454 |
455 | this.prototype = prototype;
456 | this.names = prototype.Split ('|');
457 | this.description = description;
458 | this.count = maxValueCount;
459 | this.type = ParsePrototype ();
460 |
461 | if (this.count == 0 && type != OptionValueType.None)
462 | throw new ArgumentException (
463 | "Cannot provide maxValueCount of 0 for OptionValueType.Required or " +
464 | "OptionValueType.Optional.",
465 | "maxValueCount");
466 | if (this.type == OptionValueType.None && maxValueCount > 1)
467 | throw new ArgumentException (
468 | string.Format ("Cannot provide maxValueCount of {0} for OptionValueType.None.", maxValueCount),
469 | "maxValueCount");
470 | if (Array.IndexOf (names, "<>") >= 0 &&
471 | ((names.Length == 1 && this.type != OptionValueType.None) ||
472 | (names.Length > 1 && this.MaxValueCount > 1)))
473 | throw new ArgumentException (
474 | "The default option handler '<>' cannot require values.",
475 | "prototype");
476 | }
477 |
478 | public string Prototype { get { return prototype; } }
479 |
480 | public string Description { get { return description; } }
481 |
482 | public OptionValueType OptionValueType { get { return type; } }
483 |
484 | public int MaxValueCount { get { return count; } }
485 |
486 | public string[] GetNames ()
487 | {
488 | return (string[])names.Clone ();
489 | }
490 |
491 | public string[] GetValueSeparators ()
492 | {
493 | if (separators == null)
494 | return new string [0];
495 | return (string[])separators.Clone ();
496 | }
497 |
498 | protected static T Parse (string value, OptionContext c)
499 | {
500 | Type tt = typeof(T);
501 | bool nullable = tt.IsValueType && tt.IsGenericType &&
502 | !tt.IsGenericTypeDefinition &&
503 | tt.GetGenericTypeDefinition () == typeof(Nullable<>);
504 | Type targetType = nullable ? tt.GetGenericArguments () [0] : typeof(T);
505 | TypeConverter conv = TypeDescriptor.GetConverter (targetType);
506 | T t = default (T);
507 | try {
508 | if (value != null)
509 | t = (T)conv.ConvertFromString (value);
510 | } catch (Exception e) {
511 | throw new OptionException (
512 | string.Format (
513 | c.OptionSet.MessageLocalizer ("Could not convert string `{0}' to type {1} for option `{2}'."),
514 | value, targetType.Name, c.OptionName),
515 | c.OptionName, e);
516 | }
517 | return t;
518 | }
519 |
520 | internal string[] Names { get { return names; } }
521 |
522 | internal string[] ValueSeparators { get { return separators; } }
523 |
524 | static readonly char[] NameTerminator = new char[]{'=', ':'};
525 |
526 | private OptionValueType ParsePrototype ()
527 | {
528 | char type = '\0';
529 | List seps = new List ();
530 | for (int i = 0; i < names.Length; ++i) {
531 | string name = names [i];
532 | if (name.Length == 0)
533 | throw new ArgumentException ("Empty option names are not supported.", "prototype");
534 |
535 | int end = name.IndexOfAny (NameTerminator);
536 | if (end == -1)
537 | continue;
538 | names [i] = name.Substring (0, end);
539 | if (type == '\0' || type == name [end])
540 | type = name [end];
541 | else
542 | throw new ArgumentException (
543 | string.Format ("Conflicting option types: '{0}' vs. '{1}'.", type, name [end]),
544 | "prototype");
545 | AddSeparators (name, end, seps);
546 | }
547 |
548 | if (type == '\0')
549 | return OptionValueType.None;
550 |
551 | if (count <= 1 && seps.Count != 0)
552 | throw new ArgumentException (
553 | string.Format ("Cannot provide key/value separators for Options taking {0} value(s).", count),
554 | "prototype");
555 | if (count > 1) {
556 | if (seps.Count == 0)
557 | this.separators = new string[]{":", "="};
558 | else if (seps.Count == 1 && seps [0].Length == 0)
559 | this.separators = null;
560 | else
561 | this.separators = seps.ToArray ();
562 | }
563 |
564 | return type == '=' ? OptionValueType.Required : OptionValueType.Optional;
565 | }
566 |
567 | private static void AddSeparators (string name, int end, ICollection seps)
568 | {
569 | int start = -1;
570 | for (int i = end+1; i < name.Length; ++i) {
571 | switch (name [i]) {
572 | case '{':
573 | if (start != -1)
574 | throw new ArgumentException (
575 | string.Format ("Ill-formed name/value separator found in \"{0}\".", name),
576 | "prototype");
577 | start = i + 1;
578 | break;
579 | case '}':
580 | if (start == -1)
581 | throw new ArgumentException (
582 | string.Format ("Ill-formed name/value separator found in \"{0}\".", name),
583 | "prototype");
584 | seps.Add (name.Substring (start, i - start));
585 | start = -1;
586 | break;
587 | default:
588 | if (start == -1)
589 | seps.Add (name [i].ToString ());
590 | break;
591 | }
592 | }
593 | if (start != -1)
594 | throw new ArgumentException (
595 | string.Format ("Ill-formed name/value separator found in \"{0}\".", name),
596 | "prototype");
597 | }
598 |
599 | public void Invoke (OptionContext c)
600 | {
601 | OnParseComplete (c);
602 | c.OptionName = null;
603 | c.Option = null;
604 | c.OptionValues.Clear ();
605 | }
606 |
607 | protected abstract void OnParseComplete (OptionContext c);
608 |
609 | public override string ToString ()
610 | {
611 | return Prototype;
612 | }
613 | }
614 |
615 | public abstract class ArgumentSource
616 | {
617 |
618 | protected ArgumentSource ()
619 | {
620 | }
621 |
622 | public abstract string[] GetNames ();
623 |
624 | public abstract string Description { get; }
625 |
626 | public abstract bool GetArguments (string value, out IEnumerable replacement);
627 |
628 | public static IEnumerable GetArgumentsFromFile (string file)
629 | {
630 | return GetArguments (File.OpenText (file), true);
631 | }
632 |
633 | public static IEnumerable GetArguments (TextReader reader)
634 | {
635 | return GetArguments (reader, false);
636 | }
637 |
638 | // Cribbed from mcs/driver.cs:LoadArgs(string)
639 | static IEnumerable GetArguments (TextReader reader, bool close)
640 | {
641 | try {
642 | StringBuilder arg = new StringBuilder ();
643 |
644 | string line;
645 | while ((line = reader.ReadLine ()) != null) {
646 | int t = line.Length;
647 |
648 | for (int i = 0; i < t; i++) {
649 | char c = line [i];
650 |
651 | if (c == '"' || c == '\'') {
652 | char end = c;
653 |
654 | for (i++; i < t; i++) {
655 | c = line [i];
656 |
657 | if (c == end)
658 | break;
659 | arg.Append (c);
660 | }
661 | } else if (c == ' ') {
662 | if (arg.Length > 0) {
663 | yield return arg.ToString ();
664 | arg.Length = 0;
665 | }
666 | } else
667 | arg.Append (c);
668 | }
669 | if (arg.Length > 0) {
670 | yield return arg.ToString ();
671 | arg.Length = 0;
672 | }
673 | }
674 | } finally {
675 | if (close)
676 | reader.Close ();
677 | }
678 | }
679 | }
680 |
681 | public class ResponseFileSource : ArgumentSource
682 | {
683 |
684 | public override string[] GetNames ()
685 | {
686 | return new string[]{"@file"};
687 | }
688 |
689 | public override string Description {
690 | get { return "Read response file for more options.";}
691 | }
692 |
693 | public override bool GetArguments (string value, out IEnumerable replacement)
694 | {
695 | if (string.IsNullOrEmpty (value) || !value.StartsWith ("@")) {
696 | replacement = null;
697 | return false;
698 | }
699 | replacement = ArgumentSource.GetArgumentsFromFile (value.Substring (1));
700 | return true;
701 | }
702 | }
703 |
704 | [Serializable]
705 | public class OptionException : Exception
706 | {
707 | private string option;
708 |
709 | public OptionException ()
710 | {
711 | }
712 |
713 | public OptionException (string message, string optionName)
714 | : base (message)
715 | {
716 | this.option = optionName;
717 | }
718 |
719 | public OptionException (string message, string optionName, Exception innerException)
720 | : base (message, innerException)
721 | {
722 | this.option = optionName;
723 | }
724 |
725 | protected OptionException (SerializationInfo info, StreamingContext context)
726 | : base (info, context)
727 | {
728 | this.option = info.GetString ("OptionName");
729 | }
730 |
731 | public string OptionName {
732 | get { return this.option;}
733 | }
734 |
735 | [SecurityPermission (SecurityAction.LinkDemand, SerializationFormatter = true)]
736 | public override void GetObjectData (SerializationInfo info, StreamingContext context)
737 | {
738 | base.GetObjectData (info, context);
739 | info.AddValue ("OptionName", option);
740 | }
741 | }
742 |
743 | public delegate void OptionAction (TKey key,TValue value);
744 |
745 | public class OptionSet : KeyedCollection
746 | {
747 | public OptionSet ()
748 | : this (delegate (string f) {return f;})
749 | {
750 | }
751 |
752 | public OptionSet (Converter localizer)
753 | {
754 | this.localizer = localizer;
755 | this.roSources = new ReadOnlyCollection (sources);
756 | }
757 |
758 | Converter localizer;
759 |
760 | public Converter MessageLocalizer {
761 | get { return localizer;}
762 | }
763 |
764 | List sources = new List ();
765 | ReadOnlyCollection roSources;
766 |
767 | public ReadOnlyCollection ArgumentSources {
768 | get { return roSources;}
769 | }
770 |
771 | protected override string GetKeyForItem (Option item)
772 | {
773 | if (item == null)
774 | throw new ArgumentNullException ("option");
775 | if (item.Names != null && item.Names.Length > 0)
776 | return item.Names [0];
777 | // This should never happen, as it's invalid for Option to be
778 | // constructed w/o any names.
779 | throw new InvalidOperationException ("Option has no names!");
780 | }
781 |
782 | [Obsolete ("Use KeyedCollection.this[string]")]
783 | protected Option GetOptionForName (string option)
784 | {
785 | if (option == null)
786 | throw new ArgumentNullException ("option");
787 | try {
788 | return base [option];
789 | } catch (KeyNotFoundException) {
790 | return null;
791 | }
792 | }
793 |
794 | protected override void InsertItem (int index, Option item)
795 | {
796 | base.InsertItem (index, item);
797 | AddImpl (item);
798 | }
799 |
800 | protected override void RemoveItem (int index)
801 | {
802 | Option p = Items [index];
803 | base.RemoveItem (index);
804 | // KeyedCollection.RemoveItem() handles the 0th item
805 | for (int i = 1; i < p.Names.Length; ++i) {
806 | Dictionary.Remove (p.Names [i]);
807 | }
808 | }
809 |
810 | protected override void SetItem (int index, Option item)
811 | {
812 | base.SetItem (index, item);
813 | AddImpl (item);
814 | }
815 |
816 | private void AddImpl (Option option)
817 | {
818 | if (option == null)
819 | throw new ArgumentNullException ("option");
820 | List added = new List (option.Names.Length);
821 | try {
822 | // KeyedCollection.InsertItem/SetItem handle the 0th name.
823 | for (int i = 1; i < option.Names.Length; ++i) {
824 | Dictionary.Add (option.Names [i], option);
825 | added.Add (option.Names [i]);
826 | }
827 | } catch (Exception) {
828 | foreach (string name in added)
829 | Dictionary.Remove (name);
830 | throw;
831 | }
832 | }
833 |
834 | public new OptionSet Add (Option option)
835 | {
836 | base.Add (option);
837 | return this;
838 | }
839 |
840 | sealed class ActionOption : Option
841 | {
842 | Action action;
843 |
844 | public ActionOption (string prototype, string description, int count, Action action)
845 | : base (prototype, description, count)
846 | {
847 | if (action == null)
848 | throw new ArgumentNullException ("action");
849 | this.action = action;
850 | }
851 |
852 | protected override void OnParseComplete (OptionContext c)
853 | {
854 | action (c.OptionValues);
855 | }
856 | }
857 |
858 | public OptionSet Add (string prototype, Action action)
859 | {
860 | return Add (prototype, null, action);
861 | }
862 |
863 | public OptionSet Add (string prototype, string description, Action action)
864 | {
865 | if (action == null)
866 | throw new ArgumentNullException ("action");
867 | Option p = new ActionOption (prototype, description, 1,
868 | delegate (OptionValueCollection v) {
869 | action (v [0]); });
870 | base.Add (p);
871 | return this;
872 | }
873 |
874 | public OptionSet Add (string prototype, OptionAction action)
875 | {
876 | return Add (prototype, null, action);
877 | }
878 |
879 | public OptionSet Add (string prototype, string description, OptionAction action)
880 | {
881 | if (action == null)
882 | throw new ArgumentNullException ("action");
883 | Option p = new ActionOption (prototype, description, 2,
884 | delegate (OptionValueCollection v) {
885 | action (v [0], v [1]);});
886 | base.Add (p);
887 | return this;
888 | }
889 |
890 | sealed class ActionOption : Option
891 | {
892 | Action action;
893 |
894 | public ActionOption (string prototype, string description, Action action)
895 | : base (prototype, description, 1)
896 | {
897 | if (action == null)
898 | throw new ArgumentNullException ("action");
899 | this.action = action;
900 | }
901 |
902 | protected override void OnParseComplete (OptionContext c)
903 | {
904 | action (Parse (c.OptionValues [0], c));
905 | }
906 | }
907 |
908 | sealed class ActionOption : Option
909 | {
910 | OptionAction action;
911 |
912 | public ActionOption (string prototype, string description, OptionAction action)
913 | : base (prototype, description, 2)
914 | {
915 | if (action == null)
916 | throw new ArgumentNullException ("action");
917 | this.action = action;
918 | }
919 |
920 | protected override void OnParseComplete (OptionContext c)
921 | {
922 | action (
923 | Parse (c.OptionValues [0], c),
924 | Parse (c.OptionValues [1], c));
925 | }
926 | }
927 |
928 | public OptionSet Add (string prototype, Action action)
929 | {
930 | return Add (prototype, null, action);
931 | }
932 |
933 | public OptionSet Add (string prototype, string description, Action action)
934 | {
935 | return Add (new ActionOption (prototype, description, action));
936 | }
937 |
938 | public OptionSet Add (string prototype, OptionAction action)
939 | {
940 | return Add (prototype, null, action);
941 | }
942 |
943 | public OptionSet Add (string prototype, string description, OptionAction action)
944 | {
945 | return Add (new ActionOption (prototype, description, action));
946 | }
947 |
948 | public OptionSet Add (ArgumentSource source)
949 | {
950 | if (source == null)
951 | throw new ArgumentNullException ("source");
952 | sources.Add (source);
953 | return this;
954 | }
955 |
956 | protected virtual OptionContext CreateOptionContext ()
957 | {
958 | return new OptionContext (this);
959 | }
960 |
961 | public List Parse (IEnumerable arguments)
962 | {
963 | if (arguments == null)
964 | throw new ArgumentNullException ("arguments");
965 | OptionContext c = CreateOptionContext ();
966 | c.OptionIndex = -1;
967 | bool process = true;
968 | List unprocessed = new List ();
969 | Option def = Contains ("<>") ? this ["<>"] : null;
970 | ArgumentEnumerator ae = new ArgumentEnumerator (arguments);
971 | foreach (string argument in ae) {
972 | ++c.OptionIndex;
973 | if (argument == "--") {
974 | process = false;
975 | continue;
976 | }
977 | if (!process) {
978 | Unprocessed (unprocessed, def, c, argument);
979 | continue;
980 | }
981 | if (AddSource (ae, argument))
982 | continue;
983 | if (!Parse (argument, c))
984 | Unprocessed (unprocessed, def, c, argument);
985 | }
986 | if (c.Option != null)
987 | c.Option.Invoke (c);
988 | return unprocessed;
989 | }
990 |
991 | class ArgumentEnumerator : IEnumerable
992 | {
993 | List> sources = new List> ();
994 |
995 | public ArgumentEnumerator (IEnumerable arguments)
996 | {
997 | sources.Add (arguments.GetEnumerator ());
998 | }
999 |
1000 | public void Add (IEnumerable arguments)
1001 | {
1002 | sources.Add (arguments.GetEnumerator ());
1003 | }
1004 |
1005 | public IEnumerator GetEnumerator ()
1006 | {
1007 | do {
1008 | IEnumerator c = sources [sources.Count - 1];
1009 | if (c.MoveNext ())
1010 | yield return c.Current;
1011 | else {
1012 | c.Dispose ();
1013 | sources.RemoveAt (sources.Count - 1);
1014 | }
1015 | } while (sources.Count > 0);
1016 | }
1017 |
1018 | IEnumerator IEnumerable.GetEnumerator ()
1019 | {
1020 | return GetEnumerator ();
1021 | }
1022 | }
1023 |
1024 | bool AddSource (ArgumentEnumerator ae, string argument)
1025 | {
1026 | foreach (ArgumentSource source in sources) {
1027 | IEnumerable replacement;
1028 | if (!source.GetArguments (argument, out replacement))
1029 | continue;
1030 | ae.Add (replacement);
1031 | return true;
1032 | }
1033 | return false;
1034 | }
1035 |
1036 | private static bool Unprocessed (ICollection extra, Option def, OptionContext c, string argument)
1037 | {
1038 | if (def == null) {
1039 | extra.Add (argument);
1040 | return false;
1041 | }
1042 | c.OptionValues.Add (argument);
1043 | c.Option = def;
1044 | c.Option.Invoke (c);
1045 | return false;
1046 | }
1047 |
1048 | private readonly Regex ValueOption = new Regex (
1049 | @"^(?--|-|/)(?[^:=]+)((?[:=])(?.*))?$");
1050 |
1051 | protected bool GetOptionParts (string argument, out string flag, out string name, out string sep, out string value)
1052 | {
1053 | if (argument == null)
1054 | throw new ArgumentNullException ("argument");
1055 |
1056 | flag = name = sep = value = null;
1057 | Match m = ValueOption.Match (argument);
1058 | if (!m.Success) {
1059 | return false;
1060 | }
1061 | flag = m.Groups ["flag"].Value;
1062 | name = m.Groups ["name"].Value;
1063 | if (m.Groups ["sep"].Success && m.Groups ["value"].Success) {
1064 | sep = m.Groups ["sep"].Value;
1065 | value = m.Groups ["value"].Value;
1066 | }
1067 | return true;
1068 | }
1069 |
1070 | protected virtual bool Parse (string argument, OptionContext c)
1071 | {
1072 | if (c.Option != null) {
1073 | ParseValue (argument, c);
1074 | return true;
1075 | }
1076 |
1077 | string f, n, s, v;
1078 | if (!GetOptionParts (argument, out f, out n, out s, out v))
1079 | return false;
1080 |
1081 | Option p;
1082 | if (Contains (n)) {
1083 | p = this [n];
1084 | c.OptionName = f + n;
1085 | c.Option = p;
1086 | switch (p.OptionValueType) {
1087 | case OptionValueType.None:
1088 | c.OptionValues.Add (n);
1089 | c.Option.Invoke (c);
1090 | break;
1091 | case OptionValueType.Optional:
1092 | case OptionValueType.Required:
1093 | ParseValue (v, c);
1094 | break;
1095 | }
1096 | return true;
1097 | }
1098 | // no match; is it a bool option?
1099 | if (ParseBool (argument, n, c))
1100 | return true;
1101 | // is it a bundled option?
1102 | if (ParseBundledValue (f, string.Concat (n + s + v), c))
1103 | return true;
1104 |
1105 | return false;
1106 | }
1107 |
1108 | private void ParseValue (string option, OptionContext c)
1109 | {
1110 | if (option != null)
1111 | foreach (string o in c.Option.ValueSeparators != null
1112 | ? option.Split (c.Option.ValueSeparators, c.Option.MaxValueCount - c.OptionValues.Count, StringSplitOptions.None)
1113 | : new string[]{option}) {
1114 | c.OptionValues.Add (o);
1115 | }
1116 | if (c.OptionValues.Count == c.Option.MaxValueCount ||
1117 | c.Option.OptionValueType == OptionValueType.Optional)
1118 | c.Option.Invoke (c);
1119 | else if (c.OptionValues.Count > c.Option.MaxValueCount) {
1120 | throw new OptionException (localizer (string.Format (
1121 | "Error: Found {0} option values when expecting {1}.",
1122 | c.OptionValues.Count, c.Option.MaxValueCount)),
1123 | c.OptionName);
1124 | }
1125 | }
1126 |
1127 | private bool ParseBool (string option, string n, OptionContext c)
1128 | {
1129 | Option p;
1130 | string rn;
1131 | if (n.Length >= 1 && (n [n.Length - 1] == '+' || n [n.Length - 1] == '-') &&
1132 | Contains ((rn = n.Substring (0, n.Length - 1)))) {
1133 | p = this [rn];
1134 | string v = n [n.Length - 1] == '+' ? option : null;
1135 | c.OptionName = option;
1136 | c.Option = p;
1137 | c.OptionValues.Add (v);
1138 | p.Invoke (c);
1139 | return true;
1140 | }
1141 | return false;
1142 | }
1143 |
1144 | private bool ParseBundledValue (string f, string n, OptionContext c)
1145 | {
1146 | if (f != "-")
1147 | return false;
1148 | for (int i = 0; i < n.Length; ++i) {
1149 | Option p;
1150 | string opt = f + n [i].ToString ();
1151 | string rn = n [i].ToString ();
1152 | if (!Contains (rn)) {
1153 | if (i == 0)
1154 | return false;
1155 | throw new OptionException (string.Format (localizer (
1156 | "Cannot bundle unregistered option '{0}'."), opt), opt);
1157 | }
1158 | p = this [rn];
1159 | switch (p.OptionValueType) {
1160 | case OptionValueType.None:
1161 | Invoke (c, opt, n, p);
1162 | break;
1163 | case OptionValueType.Optional:
1164 | case OptionValueType.Required: {
1165 | string v = n.Substring (i + 1);
1166 | c.Option = p;
1167 | c.OptionName = opt;
1168 | ParseValue (v.Length != 0 ? v : null, c);
1169 | return true;
1170 | }
1171 | default:
1172 | throw new InvalidOperationException ("Unknown OptionValueType: " + p.OptionValueType);
1173 | }
1174 | }
1175 | return true;
1176 | }
1177 |
1178 | private static void Invoke (OptionContext c, string name, string value, Option option)
1179 | {
1180 | c.OptionName = name;
1181 | c.Option = option;
1182 | c.OptionValues.Add (value);
1183 | option.Invoke (c);
1184 | }
1185 |
1186 | private const int OptionWidth = 29;
1187 |
1188 | public void WriteOptionDescriptions (TextWriter o)
1189 | {
1190 | foreach (Option p in this) {
1191 | int written = 0;
1192 | if (!WriteOptionPrototype (o, p, ref written))
1193 | continue;
1194 |
1195 | if (written < OptionWidth)
1196 | o.Write (new string (' ', OptionWidth - written));
1197 | else {
1198 | o.WriteLine ();
1199 | o.Write (new string (' ', OptionWidth));
1200 | }
1201 |
1202 | bool indent = false;
1203 | string prefix = new string (' ', OptionWidth + 2);
1204 | foreach (string line in GetLines (localizer (GetDescription (p.Description)))) {
1205 | if (indent)
1206 | o.Write (prefix);
1207 | o.WriteLine (line);
1208 | indent = true;
1209 | }
1210 | }
1211 |
1212 | foreach (ArgumentSource s in sources) {
1213 | string[] names = s.GetNames ();
1214 | if (names == null || names.Length == 0)
1215 | continue;
1216 |
1217 | int written = 0;
1218 |
1219 | Write (o, ref written, " ");
1220 | Write (o, ref written, names [0]);
1221 | for (int i = 1; i < names.Length; ++i) {
1222 | Write (o, ref written, ", ");
1223 | Write (o, ref written, names [i]);
1224 | }
1225 |
1226 | if (written < OptionWidth)
1227 | o.Write (new string (' ', OptionWidth - written));
1228 | else {
1229 | o.WriteLine ();
1230 | o.Write (new string (' ', OptionWidth));
1231 | }
1232 |
1233 | bool indent = false;
1234 | string prefix = new string (' ', OptionWidth + 2);
1235 | foreach (string line in GetLines (localizer (GetDescription (s.Description)))) {
1236 | if (indent)
1237 | o.Write (prefix);
1238 | o.WriteLine (line);
1239 | indent = true;
1240 | }
1241 | }
1242 | }
1243 |
1244 | bool WriteOptionPrototype (TextWriter o, Option p, ref int written)
1245 | {
1246 | string[] names = p.Names;
1247 |
1248 | int i = GetNextOptionIndex (names, 0);
1249 | if (i == names.Length)
1250 | return false;
1251 |
1252 | if (names [i].Length == 1) {
1253 | Write (o, ref written, " -");
1254 | Write (o, ref written, names [0]);
1255 | } else {
1256 | Write (o, ref written, " --");
1257 | Write (o, ref written, names [0]);
1258 | }
1259 |
1260 | for (i = GetNextOptionIndex (names, i+1);
1261 | i < names.Length; i = GetNextOptionIndex (names, i+1)) {
1262 | Write (o, ref written, ", ");
1263 | Write (o, ref written, names [i].Length == 1 ? "-" : "--");
1264 | Write (o, ref written, names [i]);
1265 | }
1266 |
1267 | if (p.OptionValueType == OptionValueType.Optional ||
1268 | p.OptionValueType == OptionValueType.Required) {
1269 | if (p.OptionValueType == OptionValueType.Optional) {
1270 | Write (o, ref written, localizer ("["));
1271 | }
1272 | Write (o, ref written, localizer ("=" + GetArgumentName (0, p.MaxValueCount, p.Description)));
1273 | string sep = p.ValueSeparators != null && p.ValueSeparators.Length > 0
1274 | ? p.ValueSeparators [0]
1275 | : " ";
1276 | for (int c = 1; c < p.MaxValueCount; ++c) {
1277 | Write (o, ref written, localizer (sep + GetArgumentName (c, p.MaxValueCount, p.Description)));
1278 | }
1279 | if (p.OptionValueType == OptionValueType.Optional) {
1280 | Write (o, ref written, localizer ("]"));
1281 | }
1282 | }
1283 | return true;
1284 | }
1285 |
1286 | static int GetNextOptionIndex (string[] names, int i)
1287 | {
1288 | while (i < names.Length && names [i] == "<>") {
1289 | ++i;
1290 | }
1291 | return i;
1292 | }
1293 |
1294 | static void Write (TextWriter o, ref int n, string s)
1295 | {
1296 | n += s.Length;
1297 | o.Write (s);
1298 | }
1299 |
1300 | private static string GetArgumentName (int index, int maxIndex, string description)
1301 | {
1302 | if (description == null)
1303 | return maxIndex == 1 ? "VALUE" : "VALUE" + (index + 1);
1304 | string[] nameStart;
1305 | if (maxIndex == 1)
1306 | nameStart = new string[]{"{0:", "{"};
1307 | else
1308 | nameStart = new string[]{"{" + index + ":"};
1309 | for (int i = 0; i < nameStart.Length; ++i) {
1310 | int start, j = 0;
1311 | do {
1312 | start = description.IndexOf (nameStart [i], j);
1313 | } while (start >= 0 && j != 0 ? description [j++ - 1] == '{' : false);
1314 | if (start == -1)
1315 | continue;
1316 | int end = description.IndexOf ("}", start);
1317 | if (end == -1)
1318 | continue;
1319 | return description.Substring (start + nameStart [i].Length, end - start - nameStart [i].Length);
1320 | }
1321 | return maxIndex == 1 ? "VALUE" : "VALUE" + (index + 1);
1322 | }
1323 |
1324 | private static string GetDescription (string description)
1325 | {
1326 | if (description == null)
1327 | return string.Empty;
1328 | StringBuilder sb = new StringBuilder (description.Length);
1329 | int start = -1;
1330 | for (int i = 0; i < description.Length; ++i) {
1331 | switch (description [i]) {
1332 | case '{':
1333 | if (i == start) {
1334 | sb.Append ('{');
1335 | start = -1;
1336 | } else if (start < 0)
1337 | start = i + 1;
1338 | break;
1339 | case '}':
1340 | if (start < 0) {
1341 | if ((i + 1) == description.Length || description [i + 1] != '}')
1342 | throw new InvalidOperationException ("Invalid option description: " + description);
1343 | ++i;
1344 | sb.Append ("}");
1345 | } else {
1346 | sb.Append (description.Substring (start, i - start));
1347 | start = -1;
1348 | }
1349 | break;
1350 | case ':':
1351 | if (start < 0)
1352 | goto default;
1353 | start = i + 1;
1354 | break;
1355 | default:
1356 | if (start < 0)
1357 | sb.Append (description [i]);
1358 | break;
1359 | }
1360 | }
1361 | return sb.ToString ();
1362 | }
1363 |
1364 | private static IEnumerable GetLines (string description)
1365 | {
1366 | return StringCoda.WrappedLines (description,
1367 | 80 - OptionWidth,
1368 | 80 - OptionWidth - 2);
1369 | }
1370 | }
1371 | }
1372 |
1373 |
--------------------------------------------------------------------------------