Skip to content

Commit 088f4ac

Browse files
committed
init: parse route from URL
Make the '<route>' positional arg optional in 'git-bundle-server init'. If it is not specified (or is an empty string), extract a route from the supplied clone URI. Called by 'init', 'core.GetRouteFromUrl()' attempts to match the URL against SSH, HTTP(S), and filesystem ('file://') URLs. If the URL cannot be matched, 'init' will exit with an error and usage printout. Also include unit tests of the route matching function to demonstrate its behavior. Signed-off-by: Victoria Dye <[email protected]>
1 parent 2b7b894 commit 088f4ac

File tree

5 files changed

+171
-10
lines changed

5 files changed

+171
-10
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ being managed by the bundle server.
104104
server route to find the data for this repository. Otherwise, the route is
105105
inferred from `<url>` by removing the domain name. For example,
106106
`https://github.com/git-for-windows/git` is assigned the route
107-
`/git-for-windows/git`. Run `git-bundle-server update` to initialize bundle
107+
`git-for-windows/git`. Run `git-bundle-server update` to initialize bundle
108108
information. Configure the web server to recognize this repository at that
109109
route. Configure scheduler to run `git-bundle-server update-all` as
110110
necessary.

cmd/git-bundle-server/init.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,20 @@ should be hosted at '<route>'.`
3535
}
3636

3737
func (i *initCmd) Run(ctx context.Context, args []string) error {
38-
parser := argparse.NewArgParser(i.logger, "git-bundle-server init <url> <route>")
38+
parser := argparse.NewArgParser(i.logger, "git-bundle-server init <url> [<route>]")
3939
url := parser.PositionalString("url", "the URL of a repository to clone", true)
40-
// TODO: allow parsing <route> out of <url>
41-
route := parser.PositionalString("route", "the route to host the specified repo", true)
40+
route := parser.PositionalString("route", "the route to host the specified repo", false)
4241
parser.Parse(ctx, args)
4342

43+
// Set route value, if needed
44+
if *route == "" {
45+
var ok bool
46+
*route, ok = core.GetRouteFromUrl(*url)
47+
if !ok {
48+
parser.Usage(ctx, "Cannot parse route from url '%s'; please specify an explicit route.", *url)
49+
}
50+
}
51+
4452
repoProvider := utils.GetDependency[core.RepositoryProvider](ctx, i.container)
4553
bundleProvider := utils.GetDependency[bundles.BundleProvider](ctx, i.container)
4654
gitHelper := utils.GetDependency[git.GitHelper](ctx, i.container)

docs/man/git-bundle-server.adoc

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,17 @@ the server.
5353

5454
== COMMANDS
5555

56-
*init* _url_ _route_::
56+
*init* _url_ [_route_]::
5757
Initialize a repository for which bundles should be served. The repository is
5858
cloned into a bare repo from _url_. A base bundle is created for the
59-
repository, served from _route_, and the man:cron[8] global bundle update
60-
schedule is started.
59+
repository and used to initialize the bundle list. If _route_ is specified,
60+
the bundle list will be served from that route; otherwise, the route is
61+
derived from the _url_. Finally, the man:cron[8] global bundle update schedule
62+
is started.
6163
+
62-
It is recommended that users specify an SSH (rather than HTTP) URL for the
63-
_url_ argument to avoid potentially error-causing authentication prompts
64-
while fetching during scheduled bundle updates.
64+
It is recommended that users specify an SSH (rather than HTTP) URL for the _url_
65+
argument to avoid potentially error-causing authentication prompts while
66+
fetching during scheduled bundle updates.
6567

6668
*start* _route_::
6769
Start computing bundles for the repository identified by _route_. If the

internal/core/funcs.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package core
2+
3+
import (
4+
"regexp"
5+
"strings"
6+
)
7+
8+
// Standalone helper functions for core (repo, cron, etc.) functionality.
9+
10+
func GetRouteFromUrl(url string) (string, bool) {
11+
matchers := []*regexp.Regexp{
12+
// SSH, matches <username>@<domain>:<route>[.git]
13+
regexp.MustCompile(`^[\w-]+@[\w\.-]+:([\w\.-]+/[\w\.-]+)/*$`),
14+
15+
// HTTP(S), matches http[s]://<domain>/<route>[.git]
16+
regexp.MustCompile(`^(?i:http[s]?)://[\w\.-]+/([\w\.-]+/[\w\.-]+)/*$`),
17+
18+
// Filesystem, matches file://[<path>/]<route>[.git]
19+
regexp.MustCompile(`^(?i:file)://[\w\.-/ ]*/([\w\.-]+/[\w\.-]+)/*$`),
20+
}
21+
22+
for _, matcher := range matchers {
23+
if groups := matcher.FindStringSubmatch(url); groups != nil {
24+
return strings.TrimSuffix(groups[1], ".git"), true
25+
}
26+
}
27+
28+
return "", false
29+
}

internal/core/funcs_test.go

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package core_test
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/github/git-bundle-server/internal/core"
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
var urlToRouteTests = []struct {
12+
url string
13+
expectedRoute string
14+
expectedMatch bool
15+
}{
16+
// SSH tests
17+
{
18+
"[email protected]:git/git.git",
19+
"git/git",
20+
true,
21+
},
22+
{
23+
"[email protected]:BUNDLE_SERVER.io/git-bundle-server",
24+
"BUNDLE_SERVER.io/git-bundle-server",
25+
true,
26+
},
27+
{
28+
"[email protected]:imFineWith/trailingSlashes//",
29+
"imFineWith/trailingSlashes",
30+
true,
31+
},
32+
{
33+
"test@mydomain:deeper/toodeep/cannotmatch",
34+
"",
35+
false,
36+
},
37+
{
38+
"[email protected]:imFineWith/trailingSlashes/",
39+
"imFineWith/trailingSlashes",
40+
true,
41+
},
42+
{
43+
"[email protected]:tooshallow",
44+
"",
45+
false,
46+
},
47+
48+
// HTTP(S) tests
49+
{
50+
"hTTp://www.mysite.net/org/repo.git/",
51+
"org/repo",
52+
true,
53+
},
54+
{
55+
"https://domain.test/clone/me",
56+
"clone/me",
57+
true,
58+
},
59+
{
60+
"https://all.my.repos/having-some_fun/with.valid_ch4racters",
61+
"having-some_fun/with.valid_ch4racters",
62+
true,
63+
},
64+
{
65+
"http://completely.normal.site/with/invalid/repo",
66+
"",
67+
false,
68+
},
69+
{
70+
"HTTPS://SCREAM/INTOTHEVOID",
71+
"",
72+
false,
73+
},
74+
75+
// Filesystem tests
76+
{
77+
"file:///root/path/to/a/repo.git",
78+
"a/repo",
79+
true,
80+
},
81+
{
82+
"FILE://RELATIVE/to/me",
83+
"to/me",
84+
true,
85+
},
86+
{
87+
"fIlE://spaces are allowed/in/path/to/repo",
88+
"to/repo",
89+
true,
90+
},
91+
{
92+
"fIlE:///butspaces/are/NOT/allowed in route",
93+
"",
94+
false,
95+
},
96+
{
97+
"file://somepathsaretooshort",
98+
"",
99+
false,
100+
},
101+
}
102+
103+
func TestGetRouteFromUrl(t *testing.T) {
104+
for _, tt := range urlToRouteTests {
105+
var title string
106+
if tt.expectedMatch {
107+
title = fmt.Sprintf("%s => %s", tt.url, tt.expectedRoute)
108+
} else {
109+
title = fmt.Sprintf("%s (no match)", tt.url)
110+
}
111+
112+
t.Run(title, func(t *testing.T) {
113+
route, isMatched := core.GetRouteFromUrl(tt.url)
114+
if tt.expectedMatch {
115+
assert.True(t, isMatched)
116+
assert.Equal(t, tt.expectedRoute, route)
117+
} else {
118+
assert.False(t, isMatched, "Expected no match, got route %s", route)
119+
}
120+
})
121+
}
122+
}

0 commit comments

Comments
 (0)