├── .gitignore ├── tests ├── fixtures │ ├── dirty-working-tree │ │ └── hello │ └── bare.git │ │ ├── HEAD │ │ ├── refs │ │ ├── heads │ │ │ └── master │ │ └── tags │ │ │ └── test-tag │ │ ├── config │ │ ├── description │ │ ├── objects │ │ ├── 7f │ │ │ └── 4d708626280d3b31928d232e4034813fe8ece5 │ │ ├── b4 │ │ │ └── d01e9b0c4a9356736dfddf8830ba9a54f5271c │ │ └── ce │ │ │ └── 013625030ba8dba906f756967f9e9ca394464a │ │ ├── hooks │ │ ├── post-commit.sample │ │ ├── post-update.sample │ │ ├── pre-applypatch.sample │ │ ├── applypatch-msg.sample │ │ ├── post-receive.sample │ │ ├── commit-msg.sample │ │ ├── prepare-commit-msg.sample │ │ ├── pre-commit.sample │ │ ├── update.sample │ │ └── pre-rebase.sample │ │ └── info │ │ └── exclude ├── simple.js └── bare.js ├── package.json ├── LICENSE ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /tests/fixtures/dirty-working-tree/hello: -------------------------------------------------------------------------------- 1 | hello 2 | -------------------------------------------------------------------------------- /tests/fixtures/bare.git/HEAD: -------------------------------------------------------------------------------- 1 | ref: refs/heads/master 2 | -------------------------------------------------------------------------------- /tests/fixtures/bare.git/refs/heads/master: -------------------------------------------------------------------------------- 1 | 7f4d708626280d3b31928d232e4034813fe8ece5 2 | -------------------------------------------------------------------------------- /tests/fixtures/bare.git/refs/tags/test-tag: -------------------------------------------------------------------------------- 1 | 7f4d708626280d3b31928d232e4034813fe8ece5 2 | -------------------------------------------------------------------------------- /tests/fixtures/bare.git/config: -------------------------------------------------------------------------------- 1 | [core] 2 | repositoryformatversion = 0 3 | filemode = true 4 | bare = true 5 | -------------------------------------------------------------------------------- /tests/fixtures/bare.git/description: -------------------------------------------------------------------------------- 1 | Unnamed repository; edit this file 'description' to name the repository. 2 | -------------------------------------------------------------------------------- /tests/simple.js: -------------------------------------------------------------------------------- 1 | require('http').createServer( 2 | require('../')(__dirname + '/..') 3 | ).listen(3000); 4 | -------------------------------------------------------------------------------- /tests/bare.js: -------------------------------------------------------------------------------- 1 | require('http').createServer( 2 | require('../')(__dirname + '/fixtures/bare.git') 3 | ).listen(3000); 4 | -------------------------------------------------------------------------------- /tests/fixtures/bare.git/objects/7f/4d708626280d3b31928d232e4034813fe8ece5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TooTallNate/node-gitProvider/HEAD/tests/fixtures/bare.git/objects/7f/4d708626280d3b31928d232e4034813fe8ece5 -------------------------------------------------------------------------------- /tests/fixtures/bare.git/objects/b4/d01e9b0c4a9356736dfddf8830ba9a54f5271c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TooTallNate/node-gitProvider/HEAD/tests/fixtures/bare.git/objects/b4/d01e9b0c4a9356736dfddf8830ba9a54f5271c -------------------------------------------------------------------------------- /tests/fixtures/bare.git/objects/ce/013625030ba8dba906f756967f9e9ca394464a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TooTallNate/node-gitProvider/HEAD/tests/fixtures/bare.git/objects/ce/013625030ba8dba906f756967f9e9ca394464a -------------------------------------------------------------------------------- /tests/fixtures/bare.git/hooks/post-commit.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script that is called after a successful 4 | # commit is made. 5 | # 6 | # To enable this hook, rename this file to "post-commit". 7 | 8 | : Nothing 9 | -------------------------------------------------------------------------------- /tests/fixtures/bare.git/hooks/post-update.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to prepare a packed repository for use over 4 | # dumb transports. 5 | # 6 | # To enable this hook, rename this file to "post-update". 7 | 8 | exec git update-server-info 9 | -------------------------------------------------------------------------------- /tests/fixtures/bare.git/info/exclude: -------------------------------------------------------------------------------- 1 | # git ls-files --others --exclude-from=.git/info/exclude 2 | # Lines that start with '#' are comments. 3 | # For a project mostly in C, the following would be a good set of 4 | # exclude patterns (uncomment them if you want to use them): 5 | # *.[oa] 6 | # *~ 7 | -------------------------------------------------------------------------------- /tests/fixtures/bare.git/hooks/pre-applypatch.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to verify what is about to be committed 4 | # by applypatch from an e-mail message. 5 | # 6 | # The hook should exit with non-zero status after issuing an 7 | # appropriate message if it wants to stop the commit. 8 | # 9 | # To enable this hook, rename this file to "pre-applypatch". 10 | 11 | . git-sh-setup 12 | test -x "$GIT_DIR/hooks/pre-commit" && 13 | exec "$GIT_DIR/hooks/pre-commit" ${1+"$@"} 14 | : 15 | -------------------------------------------------------------------------------- /tests/fixtures/bare.git/hooks/applypatch-msg.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to check the commit log message taken by 4 | # applypatch from an e-mail message. 5 | # 6 | # The hook should exit with non-zero status after issuing an 7 | # appropriate message if it wants to stop the commit. The hook is 8 | # allowed to edit the commit message file. 9 | # 10 | # To enable this hook, rename this file to "applypatch-msg". 11 | 12 | . git-sh-setup 13 | test -x "$GIT_DIR/hooks/commit-msg" && 14 | exec "$GIT_DIR/hooks/commit-msg" ${1+"$@"} 15 | : 16 | -------------------------------------------------------------------------------- /tests/fixtures/bare.git/hooks/post-receive.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script for the "post-receive" event. 4 | # 5 | # The "post-receive" script is run after receive-pack has accepted a pack 6 | # and the repository has been updated. It is passed arguments in through 7 | # stdin in the form 8 | # 9 | # For example: 10 | # aa453216d1b3e49e7f6f98441fa56946ddcd6a20 68f7abf4e6f922807889f52bc043ecd31b79f814 refs/heads/master 11 | # 12 | # see contrib/hooks/ for a sample, or uncomment the next line and 13 | # rename the file to "post-receive". 14 | 15 | #. /usr/share/doc/git-core/contrib/hooks/post-receive-email 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gitProvider", 3 | "description": "An http/connect/stack layer to serve the contents of a 'git' repository over HTTP.", 4 | "version": "0.0.4", 5 | "repository": { 6 | "type": "git", 7 | "url": "git://github.com/TooTallNate/node-gitProvider.git" 8 | }, 9 | "author": "Nathan Rajlich (http://tootallnate.net)", 10 | "main": "./index.js", 11 | "dependencies": { 12 | "gitteh": ">= 0.1.0", 13 | "mime": ">= 1.2.1" 14 | }, 15 | "directories": { 16 | "lib": "." 17 | }, 18 | "engines": { 19 | "node": ">= 0.4.0" 20 | }, 21 | "files": [ 22 | "" 23 | ], 24 | "devDependencies": {} 25 | } -------------------------------------------------------------------------------- /tests/fixtures/bare.git/hooks/commit-msg.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to check the commit log message. 4 | # Called by "git commit" with one argument, the name of the file 5 | # that has the commit message. The hook should exit with non-zero 6 | # status after issuing an appropriate message if it wants to stop the 7 | # commit. The hook is allowed to edit the commit message file. 8 | # 9 | # To enable this hook, rename this file to "commit-msg". 10 | 11 | # Uncomment the below to add a Signed-off-by line to the message. 12 | # Doing this in a hook is a bad idea in general, but the prepare-commit-msg 13 | # hook is more suited to it. 14 | # 15 | # SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') 16 | # grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" 17 | 18 | # This example catches duplicate Signed-off-by lines. 19 | 20 | test "" = "$(grep '^Signed-off-by: ' "$1" | 21 | sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || { 22 | echo >&2 Duplicate Signed-off-by lines. 23 | exit 1 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2011 Nathan Rajlich 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /tests/fixtures/bare.git/hooks/prepare-commit-msg.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to prepare the commit log message. 4 | # Called by "git commit" with the name of the file that has the 5 | # commit message, followed by the description of the commit 6 | # message's source. The hook's purpose is to edit the commit 7 | # message file. If the hook fails with a non-zero status, 8 | # the commit is aborted. 9 | # 10 | # To enable this hook, rename this file to "prepare-commit-msg". 11 | 12 | # This hook includes three examples. The first comments out the 13 | # "Conflicts:" part of a merge commit. 14 | # 15 | # The second includes the output of "git diff --name-status -r" 16 | # into the message, just before the "git status" output. It is 17 | # commented because it doesn't cope with --amend or with squashed 18 | # commits. 19 | # 20 | # The third example adds a Signed-off-by line to the message, that can 21 | # still be edited. This is rarely a good idea. 22 | 23 | case "$2,$3" in 24 | merge,) 25 | /usr/bin/perl -i.bak -ne 's/^/# /, s/^# #/#/ if /^Conflicts/ .. /#/; print' "$1" ;; 26 | 27 | # ,|template,) 28 | # /usr/bin/perl -i.bak -pe ' 29 | # print "\n" . `git diff --cached --name-status -r` 30 | # if /^#/ && $first++ == 0' "$1" ;; 31 | 32 | *) ;; 33 | esac 34 | 35 | # SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') 36 | # grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" 37 | -------------------------------------------------------------------------------- /tests/fixtures/bare.git/hooks/pre-commit.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to verify what is about to be committed. 4 | # Called by "git commit" with no arguments. The hook should 5 | # exit with non-zero status after issuing an appropriate message if 6 | # it wants to stop the commit. 7 | # 8 | # To enable this hook, rename this file to "pre-commit". 9 | 10 | if git rev-parse --verify HEAD >/dev/null 2>&1 11 | then 12 | against=HEAD 13 | else 14 | # Initial commit: diff against an empty tree object 15 | against=4b825dc642cb6eb9a060e54bf8d69288fbee4904 16 | fi 17 | 18 | # If you want to allow non-ascii filenames set this variable to true. 19 | allownonascii=$(git config hooks.allownonascii) 20 | 21 | # Cross platform projects tend to avoid non-ascii filenames; prevent 22 | # them from being added to the repository. We exploit the fact that the 23 | # printable range starts at the space character and ends with tilde. 24 | if [ "$allownonascii" != "true" ] && 25 | # Note that the use of brackets around a tr range is ok here, (it's 26 | # even required, for portability to Solaris 10's /usr/bin/tr), since 27 | # the square bracket bytes happen to fall in the designated range. 28 | test "$(git diff --cached --name-only --diff-filter=A -z $against | 29 | LC_ALL=C tr -d '[ -~]\0')" 30 | then 31 | echo "Error: Attempt to add a non-ascii file name." 32 | echo 33 | echo "This can cause problems if you want to work" 34 | echo "with people on other platforms." 35 | echo 36 | echo "To be portable it is advisable to rename the file ..." 37 | echo 38 | echo "If you know what you are doing you can disable this" 39 | echo "check using:" 40 | echo 41 | echo " git config hooks.allownonascii true" 42 | echo 43 | exit 1 44 | fi 45 | 46 | exec git diff-index --check --cached $against -- 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | node-gitProvider 2 | ======== 3 | ### An http/connect/stack layer to serve the contents of a 'git' repository over HTTP. 4 | 5 | 6 | This module offers the ability to serve the contents of a 'git' repo over HTTP, with 7 | the option to retreive files from specific commits or tags if desired. 8 | 9 | If a working directory exists in the repo, then it's contents will be served if no 10 | commit is explicitly specified. If it's a bare repo, then the _HEAD_ commit will be 11 | served when no commit is specified. 12 | 13 | 14 | Usage 15 | ----- 16 | 17 | The only required argument is the path to a git repository that you would like to serve. 18 | 19 | var http = require('http'); 20 | var gitProvider = require('gitProvider'); 21 | 22 | var server = http.createServer( 23 | gitProvider(process.env.HOME + '/someRepo') 24 | ); 25 | 26 | The barebones example above will create a git provider with the default options 27 | and will serve the git repo located a "~/someRepo". 28 | 29 | So let's assume that "someRepo" simply contains an `index.html` file and a few commits 30 | that modify that file. With the example server above running, we can send simple HTTP 31 | requests to retreive the contents of that file, in a few different ways: 32 | 33 | curl http://127.0.0.1/index.html 34 | 35 | The above command gets `index.html` from the current _HEAD_ commit of the repo (if it's 36 | a bare repo), or the file from the git working directory if it's a regular repo. 37 | 38 | curl http://127.0.0.1/f62de2f138dd241256d1cd0be10d52f671e68d2f/index.html 39 | 40 | Now the above command gets `index.html` from the specified commit: _f62de2f138dd241256d1cd0be10d52f671e68d2f_. 41 | 42 | curl http://127.0.0.1/v1.1.2/index.html 43 | 44 | `git tag`s can also be specified in place of specific commit revisions. Here _v1.1.2_ would be 45 | a git tag to reference `index.html` from. 46 | 47 | 48 | API 49 | --- 50 | 51 | ### gitProvider(repoPath [, options]) -> handler 52 | 53 | The `gitProvider` function only requires a path to a git repo. Optionally, you may 54 | pass an _options_ object that recognizes the following properties: 55 | 56 | * __mountPoint__: The request URL in which this handler will kick in. If the request 57 | URL does not begin with the value set here, the the request will be 58 | `next()`'d. (Default `/`, i.e. will always take effect). 59 | 60 | * __indexFile__: The name of the index file to serve if a directory name is specified. 61 | (Default `index.html`). 62 | 63 | 64 | [Node]: http://nodejs.org 65 | -------------------------------------------------------------------------------- /tests/fixtures/bare.git/hooks/update.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to blocks unannotated tags from entering. 4 | # Called by "git receive-pack" with arguments: refname sha1-old sha1-new 5 | # 6 | # To enable this hook, rename this file to "update". 7 | # 8 | # Config 9 | # ------ 10 | # hooks.allowunannotated 11 | # This boolean sets whether unannotated tags will be allowed into the 12 | # repository. By default they won't be. 13 | # hooks.allowdeletetag 14 | # This boolean sets whether deleting tags will be allowed in the 15 | # repository. By default they won't be. 16 | # hooks.allowmodifytag 17 | # This boolean sets whether a tag may be modified after creation. By default 18 | # it won't be. 19 | # hooks.allowdeletebranch 20 | # This boolean sets whether deleting branches will be allowed in the 21 | # repository. By default they won't be. 22 | # hooks.denycreatebranch 23 | # This boolean sets whether remotely creating branches will be denied 24 | # in the repository. By default this is allowed. 25 | # 26 | 27 | # --- Command line 28 | refname="$1" 29 | oldrev="$2" 30 | newrev="$3" 31 | 32 | # --- Safety check 33 | if [ -z "$GIT_DIR" ]; then 34 | echo "Don't run this script from the command line." >&2 35 | echo " (if you want, you could supply GIT_DIR then run" >&2 36 | echo " $0 )" >&2 37 | exit 1 38 | fi 39 | 40 | if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then 41 | echo "Usage: $0 " >&2 42 | exit 1 43 | fi 44 | 45 | # --- Config 46 | allowunannotated=$(git config --bool hooks.allowunannotated) 47 | allowdeletebranch=$(git config --bool hooks.allowdeletebranch) 48 | denycreatebranch=$(git config --bool hooks.denycreatebranch) 49 | allowdeletetag=$(git config --bool hooks.allowdeletetag) 50 | allowmodifytag=$(git config --bool hooks.allowmodifytag) 51 | 52 | # check for no description 53 | projectdesc=$(sed -e '1q' "$GIT_DIR/description") 54 | case "$projectdesc" in 55 | "Unnamed repository"* | "") 56 | echo "*** Project description file hasn't been set" >&2 57 | exit 1 58 | ;; 59 | esac 60 | 61 | # --- Check types 62 | # if $newrev is 0000...0000, it's a commit to delete a ref. 63 | zero="0000000000000000000000000000000000000000" 64 | if [ "$newrev" = "$zero" ]; then 65 | newrev_type=delete 66 | else 67 | newrev_type=$(git cat-file -t $newrev) 68 | fi 69 | 70 | case "$refname","$newrev_type" in 71 | refs/tags/*,commit) 72 | # un-annotated tag 73 | short_refname=${refname##refs/tags/} 74 | if [ "$allowunannotated" != "true" ]; then 75 | echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2 76 | echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2 77 | exit 1 78 | fi 79 | ;; 80 | refs/tags/*,delete) 81 | # delete tag 82 | if [ "$allowdeletetag" != "true" ]; then 83 | echo "*** Deleting a tag is not allowed in this repository" >&2 84 | exit 1 85 | fi 86 | ;; 87 | refs/tags/*,tag) 88 | # annotated tag 89 | if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1 90 | then 91 | echo "*** Tag '$refname' already exists." >&2 92 | echo "*** Modifying a tag is not allowed in this repository." >&2 93 | exit 1 94 | fi 95 | ;; 96 | refs/heads/*,commit) 97 | # branch 98 | if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then 99 | echo "*** Creating a branch is not allowed in this repository" >&2 100 | exit 1 101 | fi 102 | ;; 103 | refs/heads/*,delete) 104 | # delete branch 105 | if [ "$allowdeletebranch" != "true" ]; then 106 | echo "*** Deleting a branch is not allowed in this repository" >&2 107 | exit 1 108 | fi 109 | ;; 110 | refs/remotes/*,commit) 111 | # tracking branch 112 | ;; 113 | refs/remotes/*,delete) 114 | # delete tracking branch 115 | if [ "$allowdeletebranch" != "true" ]; then 116 | echo "*** Deleting a tracking branch is not allowed in this repository" >&2 117 | exit 1 118 | fi 119 | ;; 120 | *) 121 | # Anything else (is there anything else?) 122 | echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2 123 | exit 1 124 | ;; 125 | esac 126 | 127 | # --- Finished 128 | exit 0 129 | -------------------------------------------------------------------------------- /tests/fixtures/bare.git/hooks/pre-rebase.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Copyright (c) 2006, 2008 Junio C Hamano 4 | # 5 | # The "pre-rebase" hook is run just before "git rebase" starts doing 6 | # its job, and can prevent the command from running by exiting with 7 | # non-zero status. 8 | # 9 | # The hook is called with the following parameters: 10 | # 11 | # $1 -- the upstream the series was forked from. 12 | # $2 -- the branch being rebased (or empty when rebasing the current branch). 13 | # 14 | # This sample shows how to prevent topic branches that are already 15 | # merged to 'next' branch from getting rebased, because allowing it 16 | # would result in rebasing already published history. 17 | 18 | publish=next 19 | basebranch="$1" 20 | if test "$#" = 2 21 | then 22 | topic="refs/heads/$2" 23 | else 24 | topic=`git symbolic-ref HEAD` || 25 | exit 0 ;# we do not interrupt rebasing detached HEAD 26 | fi 27 | 28 | case "$topic" in 29 | refs/heads/??/*) 30 | ;; 31 | *) 32 | exit 0 ;# we do not interrupt others. 33 | ;; 34 | esac 35 | 36 | # Now we are dealing with a topic branch being rebased 37 | # on top of master. Is it OK to rebase it? 38 | 39 | # Does the topic really exist? 40 | git show-ref -q "$topic" || { 41 | echo >&2 "No such branch $topic" 42 | exit 1 43 | } 44 | 45 | # Is topic fully merged to master? 46 | not_in_master=`git rev-list --pretty=oneline ^master "$topic"` 47 | if test -z "$not_in_master" 48 | then 49 | echo >&2 "$topic is fully merged to master; better remove it." 50 | exit 1 ;# we could allow it, but there is no point. 51 | fi 52 | 53 | # Is topic ever merged to next? If so you should not be rebasing it. 54 | only_next_1=`git rev-list ^master "^$topic" ${publish} | sort` 55 | only_next_2=`git rev-list ^master ${publish} | sort` 56 | if test "$only_next_1" = "$only_next_2" 57 | then 58 | not_in_topic=`git rev-list "^$topic" master` 59 | if test -z "$not_in_topic" 60 | then 61 | echo >&2 "$topic is already up-to-date with master" 62 | exit 1 ;# we could allow it, but there is no point. 63 | else 64 | exit 0 65 | fi 66 | else 67 | not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"` 68 | /usr/bin/perl -e ' 69 | my $topic = $ARGV[0]; 70 | my $msg = "* $topic has commits already merged to public branch:\n"; 71 | my (%not_in_next) = map { 72 | /^([0-9a-f]+) /; 73 | ($1 => 1); 74 | } split(/\n/, $ARGV[1]); 75 | for my $elem (map { 76 | /^([0-9a-f]+) (.*)$/; 77 | [$1 => $2]; 78 | } split(/\n/, $ARGV[2])) { 79 | if (!exists $not_in_next{$elem->[0]}) { 80 | if ($msg) { 81 | print STDERR $msg; 82 | undef $msg; 83 | } 84 | print STDERR " $elem->[1]\n"; 85 | } 86 | } 87 | ' "$topic" "$not_in_next" "$not_in_master" 88 | exit 1 89 | fi 90 | 91 | exit 0 92 | 93 | ################################################################ 94 | 95 | This sample hook safeguards topic branches that have been 96 | published from being rewound. 97 | 98 | The workflow assumed here is: 99 | 100 | * Once a topic branch forks from "master", "master" is never 101 | merged into it again (either directly or indirectly). 102 | 103 | * Once a topic branch is fully cooked and merged into "master", 104 | it is deleted. If you need to build on top of it to correct 105 | earlier mistakes, a new topic branch is created by forking at 106 | the tip of the "master". This is not strictly necessary, but 107 | it makes it easier to keep your history simple. 108 | 109 | * Whenever you need to test or publish your changes to topic 110 | branches, merge them into "next" branch. 111 | 112 | The script, being an example, hardcodes the publish branch name 113 | to be "next", but it is trivial to make it configurable via 114 | $GIT_DIR/config mechanism. 115 | 116 | With this workflow, you would want to know: 117 | 118 | (1) ... if a topic branch has ever been merged to "next". Young 119 | topic branches can have stupid mistakes you would rather 120 | clean up before publishing, and things that have not been 121 | merged into other branches can be easily rebased without 122 | affecting other people. But once it is published, you would 123 | not want to rewind it. 124 | 125 | (2) ... if a topic branch has been fully merged to "master". 126 | Then you can delete it. More importantly, you should not 127 | build on top of it -- other people may already want to 128 | change things related to the topic as patches against your 129 | "master", so if you need further changes, it is better to 130 | fork the topic (perhaps with the same name) afresh from the 131 | tip of "master". 132 | 133 | Let's look at this example: 134 | 135 | o---o---o---o---o---o---o---o---o---o "next" 136 | / / / / 137 | / a---a---b A / / 138 | / / / / 139 | / / c---c---c---c B / 140 | / / / \ / 141 | / / / b---b C \ / 142 | / / / / \ / 143 | ---o---o---o---o---o---o---o---o---o---o---o "master" 144 | 145 | 146 | A, B and C are topic branches. 147 | 148 | * A has one fix since it was merged up to "next". 149 | 150 | * B has finished. It has been fully merged up to "master" and "next", 151 | and is ready to be deleted. 152 | 153 | * C has not merged to "next" at all. 154 | 155 | We would want to allow C to be rebased, refuse A, and encourage 156 | B to be deleted. 157 | 158 | To compute (1): 159 | 160 | git rev-list ^master ^topic next 161 | git rev-list ^master next 162 | 163 | if these match, topic has not merged in next at all. 164 | 165 | To compute (2): 166 | 167 | git rev-list master..topic 168 | 169 | if this is empty, it is fully merged to "master". 170 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var url = require('url'); 3 | var path = require('path'); 4 | var mime = require('mime'); 5 | var gitteh = require('gitteh'); 6 | 7 | var GIT_DIR = '.git'; 8 | 9 | function setup(repoPath, options) { 10 | options = options || {}; 11 | options.__proto__ = setup.DEFAULTS; 12 | 13 | // 'setup' can be synchronous... 14 | var isClone; 15 | try { 16 | isClone = fs.statSync(path.join(repoPath, GIT_DIR)).isDirectory(); 17 | } catch(e) { 18 | if (e.code === 'ENOENT') { 19 | isClone = false; 20 | } else { 21 | throw e; 22 | } 23 | } 24 | if (isClone && repoPath.substring(repoPath.length-4) !== GIT_DIR) repoPath = path.join(repoPath, GIT_DIR); 25 | //console.log("Using repository:", repoPath, "isClone:", isClone); 26 | // Synchronously open the git repo 27 | var repo = gitteh.openRepository(repoPath); 28 | 29 | return function gitProvider(req, res, next) { 30 | // Ensure that 'next()' exists (in the case of a plain http.Server) 31 | next = next || function(err) { 32 | if (err) console.error(err); 33 | res.writeHead(err ? 500 : 404); 34 | res.end(err ? err.stack : "Not Found"); 35 | }; 36 | 37 | if (!req.hasOwnProperty("uri")) { req.uri = url.parse(req.url); } 38 | if (req.uri.pathname.substring(0, options.mountPoint.length) !== options.mountPoint) return next(); 39 | 40 | var gitPath = req.uri.pathname.substring(options.mountPoint.length); 41 | 42 | var parts = gitPath.split('/'); 43 | if (parts[parts.length-1].length === 0) { 44 | parts[parts.length-1] = options.indexFile; 45 | } 46 | 47 | // The first part may be a commit reference, a git tag/branch name, 48 | // or just simply the name of the file to serve from a top-level. 49 | var firstPart = parts[0]; 50 | //console.log("First Part: " + firstPart); 51 | 52 | if (firstPart.length === 40) { 53 | // Check the commit id, then git tag, then serve file 54 | checkCommit(firstPart); 55 | } else { 56 | // Check for a git tag, then serve file 57 | checkTag(firstPart); 58 | } 59 | 60 | function checkCommit(commitRef) { 61 | repo.getCommit(commitRef, function(err, commit) { 62 | if (err) { 63 | if (err.gitError === gitteh.error.GIT_ENOTFOUND) { 64 | // first part isn't a git commit, next check for a git tag 65 | return checkTag(commitRef); 66 | } else { 67 | // Some other kind of error, baaddd! 68 | return next(err); 69 | } 70 | } 71 | // If there was no error then we got a valid commit id. Serve the file from parts[1] 72 | repo.getTree(commit.tree, function(err, tree) { 73 | serveGitFile(repo, tree, parts.slice(1), res, next); 74 | }); 75 | }); 76 | } 77 | function checkTag(tag) { 78 | listTags(repo, function(err, tags) { 79 | if (err) return next(err); 80 | 81 | //console.log("Tags: " + tags); 82 | 83 | var tagIndex = tags.indexOf(firstPart); 84 | if (tagIndex >= 0) { 85 | //console.log("Found tag: " + tags[tagIndex]); 86 | 87 | // Requested a file from a git tag. Resolve the git tag then serve the file from parts[1] 88 | resolveTag(repo, tag, function(err, commitRef) { 89 | if (err) return next(err); 90 | serveCommit(commitRef, parts.slice(1)); 91 | }); 92 | } else { 93 | //console.log("Tag not found"); 94 | // No git tag, all the parts are the path of the file to server. 95 | // Serve from HEAD (bare repo) or a real file (cloned repo). 96 | if (isClone) { 97 | fs.readFile(path.join(repoPath, '..', parts.join('/')), function(err, buf) { 98 | if (err) { 99 | if (err.code === 'ENOENT') return next(); 100 | else return next(err); 101 | } 102 | serveBuffer(buf, res, gitPath); 103 | }); 104 | } else { 105 | // Is a bare repo, serve file from resolved HEAD 106 | resolveHead(repo, function(err, commitRef) { 107 | if (err) return next(err); 108 | serveCommit(commitRef, parts); 109 | }); 110 | } 111 | } 112 | }); 113 | } 114 | 115 | function serveCommit(commitRef, parts) { 116 | repo.getCommit(commitRef, function(err, commit) { 117 | if (err) return next(err); 118 | repo.getTree(commit.tree, function(err, tree) { 119 | if (err) return next(err); 120 | serveGitFile(repo, tree, parts, res, next); 121 | }); 122 | }); 123 | } 124 | } 125 | } 126 | module.exports = setup; 127 | 128 | setup.DEFAULTS = { 129 | // The base request URL to serve from 130 | mountPoint: '/', 131 | // The name of the index file to serve when 132 | // a directory name is requested 133 | indexFile: 'index.html' 134 | }; 135 | 136 | 137 | // Returns a Buffer of the specified file from the specified repo and commit reference. 138 | // Recursively gets the tree instances until the last part, which is gets the rawObject 139 | // for and serves back over HTTP. 140 | function serveGitFile(repo, tree, parts, res, next) { 141 | //console.log("Serving git file: " + parts); 142 | var thisPart = parts.shift(); 143 | var isLastPart = parts.length === 0; 144 | var entryIndex = -1; 145 | for (var i=0; i < tree.entries.length; i++) { 146 | if (tree.entries[i].name === thisPart) { 147 | entryIndex = i; 148 | break; 149 | } 150 | } 151 | if (entryIndex < 0) return next(); 152 | var entry = tree.entries[entryIndex]; 153 | if (isLastPart) { 154 | repo.getBlob(entry.id, function(err, buf) { 155 | if (err) return next(err); 156 | if (!buf.data) return next(); 157 | serveBuffer(buf.data, res, thisPart); 158 | }); 159 | } else { 160 | repo.getTree(entry.id, function(err, entryTree) { 161 | if (err) return next(err); 162 | serveGitFile(repo, entryTree, parts, res, next); 163 | }); 164 | } 165 | } 166 | 167 | 168 | function serveBuffer(buf, res, filename) { 169 | var contentType = mime.lookup(filename); 170 | res.setHeader('Content-Length', buf.length); 171 | res.setHeader('Content-Type', contentType); 172 | res.end(buf); 173 | } 174 | 175 | 176 | function resolveReference(repo, reference, callback) { 177 | repo.getReference(reference, onRef); 178 | function onRef(err, ref) { 179 | if (err) return callback(err); 180 | if (ref.type === gitteh.GIT_REF_OID) { 181 | callback(null, ref.target); 182 | } else if (ref.type === gitteh.GIT_REF_SYMBOLIC) { 183 | ref.resolve(onRef); 184 | } else { 185 | callback(new Error('Got unknown reference type: ' + ref.type)); 186 | } 187 | } 188 | } 189 | 190 | 191 | function resolveHead(repo, callback) { 192 | resolveReference(repo, 'HEAD', callback); 193 | } 194 | 195 | var TAG_REF = 'refs/tags/'; 196 | function resolveTag(repo, tag, callback) { 197 | resolveReference(repo, TAG_REF+tag, callback); 198 | } 199 | 200 | 201 | function listTags(repo, callback) { 202 | repo.listReferences(gitteh.GIT_REF_LISTALL, onList); 203 | function onList(err, refs) { 204 | if (err) return callback(err); 205 | refs = refs.filter(function(ref) { return /^refs\/tags\//.test(ref); }); 206 | refs = refs.map(function(ref) { return ref.substring(TAG_REF.length); }); 207 | callback(null, refs); 208 | } 209 | } 210 | --------------------------------------------------------------------------------