├── .gitignore ├── .gitmodules ├── README.md ├── doc ├── README-repo.txt ├── README-undo-git-breakage.txt ├── git-Subversion_bridge.png └── synchronize-git-svn.sh.log ├── git-svn-auth-manager ├── GitSvnAuthManager.csproj ├── GitSvnAuthManager.sln ├── Makefile ├── README.rst ├── config-full ├── config-just-enough ├── mail-sample.txt └── src │ ├── AssemblyInfo.cs │ ├── Config.cs │ ├── EmailSender.cs │ ├── EncryptedSQLiteDb.cs │ ├── EncryptedUserRepository.cs │ ├── GitSvnAuthManager.cs │ ├── Main.cs │ ├── Options.cs │ ├── SQLiteNativeMethods.cs │ ├── SQLiteTypeAffinity.cs │ └── User.cs ├── scripts ├── setup-svn-branch-git-bridge.sh ├── setup-svn-branch-git-bridge.sh.config ├── synchronize-git-svn.sh └── synchronize-git-svn.sh.config └── test └── run-test.sh /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.dll 3 | *.exe 4 | bin/ 5 | obj/ 6 | *.pidb 7 | *.userprefs 8 | *.db 9 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "git-svn-auth-manager/sqlcipher"] 2 | path = git-svn-auth-manager/sqlcipher 3 | url = git://github.com/sqlcipher/sqlcipher.git 4 | -------------------------------------------------------------------------------- /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 | ![image](https://raw.github.com/mrts/git-svn-bridge/master/doc/git-Subversion_bridge.png) 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /doc/git-Subversion_bridge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrts/git-svn-bridge/b7f74c6ea91acc0afcff62fd762eaacf58311b34/doc/git-Subversion_bridge.png -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/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/config-full: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /git-svn-auth-manager/config-just-enough: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------