diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 359b24b..29c9415 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -47,6 +47,10 @@ jobs: uses: actions/setup-go@v3 with: go-version: '1.19.0' + - uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.2.1' + - run: gem install asciidoctor - name: Clone repository uses: actions/checkout@v3 - name: Build the release artifact diff --git a/.gitignore b/.gitignore index ff1759d..d60a0a5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ /git-bundle-web-server /bin/ /_dist/ +/_docs/ diff --git a/.vscode/settings.json b/.vscode/settings.json index dbc338a..21f9dfc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -13,6 +13,13 @@ "editor.wordWrap": "off", "files.trimTrailingWhitespace": true, }, + "[asciidoc]": { + "editor.detectIndentation": false, + "editor.insertSpaces": true, + "editor.tabSize": 2, + "editor.wordWrap": "off", + "files.trimTrailingWhitespace": true, + }, "[shellscript]": { "editor.detectIndentation": false, "editor.insertSpaces": false, @@ -21,6 +28,8 @@ "files.trimTrailingWhitespace": true, }, "files.associations": { + "*.adoc": "asciidoc", + "*.asc": "asciidoc", "*.md": "markdown", "*.sh": "shellscript", "prerm": "shellscript", diff --git a/Makefile b/Makefile index 627b9ae..8fb9507 100644 --- a/Makefile +++ b/Makefile @@ -6,9 +6,13 @@ NAME := git-bundle-server VERSION := PACKAGE_REVISION := 1 +# Installation information +INSTALL_ROOT := / + # Helpful paths BINDIR := $(CURDIR)/bin DISTDIR := $(CURDIR)/_dist +DOCDIR := $(CURDIR)/_docs # Platform information GOOS := $(shell go env GOOS) @@ -25,6 +29,23 @@ build: @mkdir -p $(BINDIR) GOOS="$(GOOS)" GOARCH="$(GOARCH)" go build -o $(BINDIR) ./... +.PHONY: doc +doc: + @scripts/make-docs.sh --docs="$(CURDIR)/docs/man" \ + --output="$(DOCDIR)" + +# Installation targets +.PHONY: install +install: build doc + @echo + @echo "======== Installing to $(INSTALL_ROOT) ========" + @scripts/install.sh --bindir="$(BINDIR)" \ + --docdir="$(DOCDIR)" \ + --uninstaller="$(CURDIR)/scripts/uninstall.sh" \ + --allow-root \ + --include-symlinks \ + --install-root="$(INSTALL_ROOT)" + # Packaging targets .PHONY: check-arch check-arch: @@ -49,12 +70,13 @@ DEBDIR := $(DISTDIR)/deb DEB_FILENAME := $(DISTDIR)/$(NAME)_$(VERSION)-$(PACKAGE_REVISION)_$(PACKAGE_ARCH).deb # Targets -$(DEBDIR)/root: check-arch build +$(DEBDIR)/root: check-arch build doc @echo @echo "======== Formatting package contents ========" - @build/package/layout-unix.sh --bindir="$(BINDIR)" \ - --include-symlinks \ - --output="$(DEBDIR)/root" + @scripts/install.sh --bindir="$(BINDIR)" \ + --docdir="$(DOCDIR)" \ + --include-symlinks \ + --install-root="$(DEBDIR)/root" $(DEB_FILENAME): check-version $(DEBDIR)/root @echo @@ -80,12 +102,13 @@ PKGDIR := $(DISTDIR)/pkg PKG_FILENAME := $(DISTDIR)/$(NAME)_$(VERSION)-$(PACKAGE_REVISION)_$(PACKAGE_ARCH).pkg # Targets -$(PKGDIR)/payload: check-arch build +$(PKGDIR)/payload: check-arch build doc @echo @echo "======== Formatting package contents ========" - @build/package/layout-unix.sh --bindir="$(BINDIR)" \ - --uninstaller="$(CURDIR)/build/package/pkg/uninstall.sh" \ - --output="$(PKGDIR)/payload" + @scripts/install.sh --bindir="$(BINDIR)" \ + --docdir="$(DOCDIR)" \ + --uninstaller="$(CURDIR)/scripts/uninstall.sh" \ + --install-root="$(PKGDIR)/payload" $(PKG_FILENAME): check-version $(PKGDIR)/payload @echo @@ -111,3 +134,4 @@ clean: go clean ./... $(RM) -r $(BINDIR) $(RM) -r $(DISTDIR) + $(RM) -r $(DOCDIR) diff --git a/README.md b/README.md index c1b6fa0..be3636f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# `git-bundle-server`: Manage a self-hosted bundle server +# Git Bundle Server [bundle-uris]: https://github.com/git/git/blob/next/Documentation/technical/bundle-uri.txt [codeowners]: CODEOWNERS @@ -15,26 +15,86 @@ This repository is under active development, and loves contributions from the community :heart:. Check out [CONTRIBUTING][contributing] for details on getting started. -## Cloning and Building +## Getting Started -Be sure to clone inside the `src` directory of your `GOROOT`. +### Installing -Once there, you can build the `git-bundle-server` and `git-bundle-web-server` -executables with +> :warning: Installation on Windows is currently unsupported :warning: + + +[releases]: https://github.com/github/git-bundle-server/releases + +#### Linux + +Debian packages (for x86_64 systems) can be downloaded from the +[Releases][releases] page and installed with: + +```bash +sudo dpkg -i /path/to/git-bundle-server_VVV-RRR_amd64.deb + +# VVV: version +# RRR: package revision +``` + +#### MacOS + +Packages for both Intel and M1 systems can be downloaded from the +[Releases][releases] page (identified by the `amd64` vs `arm64` filename suffix, +respectively). The package can be installed by double-clicking the downloaded +file, or on the command line with: + +```bash +sudo installer -pkg /path/to/git-bundle-server_VVV-RRR_AAA.pkg -target / + +# VVV: version +# RRR: package revision +# AAA: platform architecture (amd64 or arm64) +``` + +#### From source + +> To avoid environment issues building and executing Go code, we recommend that +> you clone inside the `src` directory of your `GOROOT`. + +You can also install the bundle server application from source on any Unix-based +system. To install to the system root, clone the repository and run: ```ShellSession -$ go build -o . ./... +$ make install ``` -## Testing and Linting +Note that you will likely be prompted for a password to allow installing to +root-owned directories (e.g. `/usr/local/bin`). -To run the project's unit tests, navigate to the repository root directory and -run `go test -v ./...`. +To install somewhere other than the system root, you can manually specify an +`INSTALL_ROOT` when building the `install` target: -To run the project's linter, navigate to the repository root directory and run -`go vet ./...`. +```ShellSession +$ make install INSTALL_ROOT= +``` + +### Uninstalling + +#### From Debian package + +To uninstall `git-bundle-server` if it was installed from a Debian package, run: + +```ShellSession +$ sudo dpkg -r git-bundle-server +``` -## Bundle Management through CLI +#### Everything else + +All other installation methods include an executable script that uninstalls all +bundle server resources. On MacOS & Linux, run: + +```ShellSession +$ /usr/local/git-bundle-server/uninstall.sh +``` + +## Usage + +### Repository management The following command-line interface allows you to manage which repositories are being managed by the bundle server. @@ -73,7 +133,7 @@ being managed by the bundle server. * `git-bundle-server delete `: Remove the configuration for the given `` and delete its repository data. -## Web Server Management +### Web server management Independent of the management of the individual repositories hosted by the server, you can manage the web server process itself using these commands: @@ -85,6 +145,37 @@ server, you can manage the web server process itself using these commands: Finally, if you want to run the web server process directly in your terminal, for debugging purposes, then you can run `git-bundle-web-server`. +## Local development + +### Building + +> To avoid environment issues building and executing Go code, we recommend that +> you clone inside the `src` directory of your `GOROOT`. + +In the root of your cloned repository, you can build the `git-bundle-server` and +`git-bundle-web-server` executables a few ways. + +The first is to use GNU Make; from the root of the repository, simply run: + +```ShellSession +$ make +``` + +If you do not have `make` installed on your system, you may instead run (again +from the repository root): + +```ShellSession +$ go build -o bin/ ./... +``` + +### Testing and Linting + +To run the project's unit tests, navigate to the repository root directory and +run `go test -v ./...`. + +To run the project's linter, navigate to the repository root directory and run +`go vet ./...`. + ## License This project is licensed under the terms of the MIT open source license. Please diff --git a/build/package/layout-unix.sh b/build/package/layout-unix.sh deleted file mode 100755 index 8981a08..0000000 --- a/build/package/layout-unix.sh +++ /dev/null @@ -1,76 +0,0 @@ -#!/bin/bash -die () { - echo "$*" >&2 - exit 1 -} - -# Parse script arguments -for i in "$@" -do -case "$i" in - --bindir=*) - BINDIR="${i#*=}" - shift # past argument=value - ;; - --uninstaller=*) - UNINSTALLER="${i#*=}" - shift # past argument=value - ;; - --include-symlinks) - INCLUDE_SYMLINKS=1 - shift # past argument - ;; - --output=*) - PAYLOAD="${i#*=}" - shift # past argument=value - ;; - *) - die "unknown option '$i'" - ;; -esac -done - -# Perform pre-execution checks -if [ -z "$BINDIR" ]; then - die "--bindir was not set" -fi -if [ -z "$PAYLOAD" ]; then - die "--output was not set" -fi - -# Exit as soon as any line fails -set -e - -# Cleanup any old payload directory -if [ -d "$PAYLOAD" ]; then - echo "Cleaning old output directory '$PAYLOAD'..." - rm -rf "$PAYLOAD" -fi - -# Ensure payload directory exists -INSTALL_TO="$PAYLOAD/usr/local/git-bundle-server" -mkdir -p "$INSTALL_TO" - -# Copy built binaries -echo "Copying binaries..." -cp -R "$BINDIR/." "$INSTALL_TO/bin" - -# Copy uninstaller script -if [ -n "$UNINSTALLER" ]; then - echo "Copying uninstall script..." - cp "$UNINSTALLER" "$INSTALL_TO" -fi - -# Create symlinks -if [ -n "$INCLUDE_SYMLINKS" ]; then - LINK_TO="$PAYLOAD/usr/local/bin" - mkdir -p "$LINK_TO" - - echo "Creating symlinks..." - for program in "$INSTALL_TO"/bin/* - do - ln -s -r "$program" "$LINK_TO/$(basename $program)" - done -fi - -echo "Layout complete." diff --git a/build/package/pkg/scripts/postinstall b/build/package/pkg/scripts/postinstall index f92910b..646acca 100755 --- a/build/package/pkg/scripts/postinstall +++ b/build/package/pkg/scripts/postinstall @@ -5,15 +5,32 @@ PACKAGE=$1 INSTALL_DESTINATION=$2 # Directories -INSTALL_TO="$INSTALL_DESTINATION/usr/local/git-bundle-server/" -LINK_TO="$INSTALL_DESTINATION/usr/local/bin/" -RELATIVE_LINK_TO_INSTALL="../git-bundle-server" +APP_ROOT="$INSTALL_DESTINATION/usr/local/git-bundle-server" +LINK_TO="$INSTALL_DESTINATION/usr/local/bin" +RELATIVE_LINK_TO_BIN="../git-bundle-server/bin" mkdir -p "$LINK_TO" # Create symlinks -for program in "$INSTALL_TO"/bin/* +for program in "$APP_ROOT"/bin/* do - /bin/ln -Fs "$RELATIVE_LINK_TO_INSTALL/bin/$(basename $program)" "$LINK_TO/$(basename $program)" + p=$(basename "$program") + rm -f "$LINK_TO/$p" + ln -s "$RELATIVE_LINK_TO_BIN/$p" "$LINK_TO/$p" +done + +for mandir in "$APP_ROOT"/share/man/man*/ +do + mdir=$(basename "$mandir") + LINK_TO="$INSTALL_DESTINATION/usr/local/share/man/$mdir" + RELATIVE_LINK_TO_MAN="../../../git-bundle-server/share/man/$mdir" + mkdir -p "$LINK_TO" + + for manpage in "$mandir"/* + do + mpage=$(basename "$manpage") + rm -f "$LINK_TO/$mpage" + ln -s "$RELATIVE_LINK_TO_MAN/$mpage" "$LINK_TO/$mpage" + done done exit 0 diff --git a/build/package/pkg/uninstall.sh b/build/package/pkg/uninstall.sh deleted file mode 100644 index 04f4bab..0000000 --- a/build/package/pkg/uninstall.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/bash -set -e - -THISDIR="$( cd "$(dirname "$0")" ; pwd -P )" -PATH_TO_SYMLINKS="$THISDIR/../bin" - - -# Ensure we're running as root -if [ $(id -u) != "0" ] -then - sudo "$0" "$@" - exit $? -fi - -# Get the current logged-in user from the owner of /dev/console -LOGGED_IN_USER=$(stat -f "%Su" /dev/console) -echo "Stopping the web server daemon for user '$LOGGED_IN_USER'..." -sudo -u $LOGGED_IN_USER \ - "$THISDIR/bin/git-bundle-server" web-server stop --remove - -# Remove symlinks -for program in "$THISDIR"/bin/* -do - symlink="$PATH_TO_SYMLINKS/$(basename $program)" - if [ -L "$symlink" ] - then - echo "Deleting '$symlink'..." - rm -f "$symlink" - else - echo "No symlink found at path '$symlink'." - fi -done - -# Remove application files -if [ -d "$THISDIR" ] -then - echo "Deleting application files in '$THISDIR'..." - rm -rf "$THISDIR" -else - echo "No application files found." -fi - -# Forget package installation/delete receipt -echo "Removing installation receipt..." -pkgutil --forget com.github.gitbundleserver || echo "Could not remove package receipt. Exiting..." -exit 0 diff --git a/docs/man/README.md b/docs/man/README.md new file mode 100644 index 0000000..9b133e0 --- /dev/null +++ b/docs/man/README.md @@ -0,0 +1,18 @@ +# Man pages + +This directory contains Asciidoc-formatted files that are built and installed as +manual pages for the tools in the Git Bundle Server repository. + +## File naming +Files with the extension `.adoc` will generate matching `man` page entries +through the `make doc` target. Supplemental content for these files (utilized +via [`include` directives][include]) must _not_ use the `.adoc` extension; the +recommended extension for these files is `.asc`. + +[include]: https://docs.asciidoctor.org/asciidoc/latest/directives/include/ + +## Updating + +When major user-facing features of the repository's CLI tools are added or +changed (e.g. new options or subcommands), the corresponding `.adoc` should be +updated to reflect those changes. diff --git a/docs/man/git-bundle-server.adoc b/docs/man/git-bundle-server.adoc new file mode 100644 index 0000000..ce4d9e6 --- /dev/null +++ b/docs/man/git-bundle-server.adoc @@ -0,0 +1,153 @@ += git-bundle-server(1) +:doctype: manpage +:manmanual: Git Bundle Server Manual +:mansource: Git Bundle Server + +== NAME + +git-bundle-server - create, manage, and host Git bundle content + +== SYNOPSIS +[verse] +*git-bundle-server* _command_ [_options_] + +== DESCRIPTION + +The *git-bundle-server* command-line interface configures, generates, and hosts +Git bundles for use with Git's bundle URI feature (see man:git-bundle[1], +man:git-fetch[1]). + +A bundle server is comprised of four main components: + +1. the repositories for which bundles are generated +2. the base & incremental bundles for each repository +3. the per-repository lists of those bundles +4. the web server hosting the bundle and bundle-list content + +Repositories are initialized in the bundle server with the *init* command, which +clones a specified repository and creates an initial bundle for it. +Initialization also adds the repository to a list of repositories that are +updated by the *update-all* command on a daily man:cron[8] schedule. + +New incremental bundles are created when the repository is updated, either +manually (with an invocation of *update* or *update-all*) or automatically (via +the man:cron[8] job). The maximum number of bundles per repository is 5; rather +than creating a sixth bundle, the next update will collapse all bundles into a +new base bundle. + +Bundle generation for a repository can be stopped with the *stop* command; if a +user wishes to delete all on-disk resources for a repository, *delete* will +remove all existing bundles and internal repository clone as well. + +The bundles generated by this server make use of the 'creationToken' heuristic +to help Git clients avoid downloading bundles they already have +footnote:[Details about the 'creationToken' heuristic can be found in the Git +bundle URI technical documentation: url:https://git-scm.com/docs/bundle-uri[]]. + +To serve the generated bundles, the *web-server* command can be used to start or +stop a configured web server. The server will run in the domain of the user that +invoked the command, and will continue running after the user logs out. The +server does not automatically start on system boot (even if it was running +before prior shutdown), so *web-server start* will need to be invoked to restart +the server. + +== COMMANDS + +*init* _url_ _route_:: + Initialize a repository for which bundles should be served. The repository is + cloned into a bare repo from _url_. A base bundle is created for the + repository, served from _route_, and the man:cron[8] global bundle update + schedule is started. ++ +It is recommended that users specify an SSH (rather than HTTP) URL for the +_url_ argument to avoid potentially error-causing authentication prompts +while fetching during scheduled bundle updates. + +*start* _route_:: + Start computing bundles for the repository identified by _route_. If the + man:cron[8] scheduler responsible for periodic bundle updates has not been + configured, this command starts running a global update schedule as well. + +*stop* _route_:: + Stop computing bundles for the repository identified by _route_. + +*update* _route_:: + For the repository specified by _route_, fetch the latest content from the + remote and create a new set of bundles and update the bundle list. + +*update-all*:: + Update all initialized repositories with *git-bundle-server update*. This + command is called via the man:cron[8] scheduler. + +*delete* _route_:: + Remove a repository configuration and delete its data on disk. + +*web-server* *start* [*-f*|*--force*] [_server-options_]:: + Start a background process web server hosting bundle metadata and content. The + web server daemon runs under the calling user's domain, and will continue + running after the user logs out. ++ +By default, this command does not restart a running web server process or +overwrite an existing web server configuration. To force that behavior, use the +*--force* option. ++ +Additionally, users may provide options that configure the execution of +the man:git-bundle-web-server[1] background process. ++ +-- + *-f*::: + *--force*::: + If the web server daemon has already been configured, stop the running + process if needed and rewrite the configuration before starting the service. + Users should specify this option if they intend to change the web server + configuration (e.g., the port number). +-- ++ +*** +Server options: ++ +-- +include::server-options.asc[] +-- ++ +*** + +*web-server* *stop* [*--remove*]:: + Stop the web server background process associated with the current user, if + one is running. Unless the *--remove* option is specified, the service + configuration is left on disk and remains loaded into the system daemon + controller. + + *--remove*::: + In addition to stopping a running web server process, fully unload the + service configuration and remove any associated daemon config files from + disk. + +== EXAMPLE + +Initialize and start generating bundles for the remote repository hosted at +url:https://github.com/rust-lang/rust[]: + +[source,console] +---- +$ git-bundle-server init git@github.com:rust-lang/rust.git rust-lang/rust +---- + +Generate an incremental bundle with latest updates to rust-lang/rust: + +[source,console] +---- +$ git-bundle-server update rust-lang/rust +---- + +Start an HTTPS web server on port 443 with certificate 'server.crt' and private +key 'server.key': + +[source,console] +---- +$ git-bundle-server web-server start --force --port 443 --cert server.crt --key server.key +---- + +== SEE ALSO + +man:git-bundle-web-server[1], man:git-bundle[1], man:git-fetch[1] diff --git a/docs/man/git-bundle-web-server.adoc b/docs/man/git-bundle-web-server.adoc new file mode 100644 index 0000000..4610d6a --- /dev/null +++ b/docs/man/git-bundle-web-server.adoc @@ -0,0 +1,32 @@ += git-bundle-web-server(1) +:doctype: manpage +:manmanual: Git Bundle Server Manual +:mansource: Git Bundle Server + +== NAME + +git-bundle-web-server - run a web server hosting Git bundle content + +== SYNOPSIS +[verse] +*git-bundle-web-server* [_server-options_] + +== DESCRIPTION + +The *git-bundle-web-server* utility runs a web server on the local machine +serving Git bundle content in accordance with Git's bundle URI feature (see +man:git-bundle*[1], man:git-fetch[1]). The process operates under the assumption that +its content is generated by the man:git-bundle-server[1] CLI; unexpected behavior +may result from manually creating, replacing, or removing bundle content. + +This program should generally not be called directly by users outside of niche +debugging scenarios. Instead, users are recommended to use *git-bundle-server +web-server* for managing the web server process on their systems. + +== OPTIONS + +include::server-options.asc[] + +== SEE ALSO + +man:git-bundle-server[1], man:git-bundle[1], man:git-fetch[1] diff --git a/docs/man/server-options.asc b/docs/man/server-options.asc new file mode 100644 index 0000000..c953739 --- /dev/null +++ b/docs/man/server-options.asc @@ -0,0 +1,12 @@ +*--port* _port_::: + Configure the web server to run on the given port. By default, the port is + 8080. + +*--cert* _path_::: + Use the X.509 SSL certificate at the given path to configure the web + server for HTTPS. Must be used with a corresponding private key file + specified with *--key*. + +*--key* _path_::: + Use the contents of the specified file as the private key of the X.509 SSL + certificate specified with *--cert*. diff --git a/internal/daemon/launchd.go b/internal/daemon/launchd.go index 3627db4..235c86e 100644 --- a/internal/daemon/launchd.go +++ b/internal/daemon/launchd.go @@ -259,7 +259,9 @@ func (l *launchd) Stop(label string) error { } // Don't throw an error if the service hasn't been bootstrapped - if exitCode != 0 && exitCode != LaunchdServiceNotFoundErrorCode { + if exitCode != 0 && + exitCode != LaunchdServiceNotFoundErrorCode && + exitCode != LaunchdNoSuchProcessErrorCode { return fmt.Errorf("'launchctl kill' exited with status %d", exitCode) } diff --git a/internal/daemon/launchd_test.go b/internal/daemon/launchd_test.go index 14e42ec..59d1084 100644 --- a/internal/daemon/launchd_test.go +++ b/internal/daemon/launchd_test.go @@ -392,6 +392,44 @@ func TestLaunchd_Start(t *testing.T) { }) } +var launchdStopTests = []struct { + title string + + // Inputs + label string + + // Mocked responses + launchctlKill *Pair[int, error] + + // Expected values + expectErr bool +}{ + { + "Running service is stopped successfully", + "com.test.service", + PtrTo(NewPair[int, error](0, nil)), // launchctl kill + false, + }, + { + "Stopping service not yet bootstrapped returns no error", + "com.test.service", + PtrTo(NewPair[int, error](daemon.LaunchdServiceNotFoundErrorCode, nil)), // launchctl kill + false, + }, + { + "Stopping service not running returns no error", + "com.test.service", + PtrTo(NewPair[int, error](daemon.LaunchdNoSuchProcessErrorCode, nil)), // launchctl kill + false, + }, + { + "Unknown launchctl error throws error", + "com.test.service", + PtrTo(NewPair[int, error](-1, nil)), // launchctl kill + true, + }, +} + func TestLaunchd_Stop(t *testing.T) { // Set up mocks testUser := &user.User{ @@ -405,47 +443,29 @@ func TestLaunchd_Stop(t *testing.T) { launchd := daemon.NewLaunchdProvider(testUserProvider, testCommandExecutor, nil) - // Test #1: launchctl succeeds - t.Run("Calls correct launchctl command", func(t *testing.T) { - testCommandExecutor.On("Run", - "launchctl", - []string{"kill", "SIGINT", fmt.Sprintf("user/123/%s", basicDaemonConfig.Label)}, - ).Return(0, nil).Once() - - err := launchd.Stop(basicDaemonConfig.Label) - assert.Nil(t, err) - mock.AssertExpectationsForObjects(t, testCommandExecutor) - }) - - // Reset the mock structure between tests - testCommandExecutor.Mock = mock.Mock{} - - // Test #2: launchctl fails with uncaught error - t.Run("Returns error when launchctl fails", func(t *testing.T) { - testCommandExecutor.On("Run", - mock.AnythingOfType("string"), - mock.AnythingOfType("[]string"), - ).Return(1, nil).Once() - - err := launchd.Stop(basicDaemonConfig.Label) - assert.NotNil(t, err) - mock.AssertExpectationsForObjects(t, testCommandExecutor) - }) - - // Reset the mock structure between tests - testCommandExecutor.Mock = mock.Mock{} + for _, tt := range launchdStopTests { + t.Run(tt.title, func(t *testing.T) { + // Mock responses + if tt.launchctlKill != nil { + testCommandExecutor.On("Run", + "launchctl", + []string{"kill", "SIGINT", fmt.Sprintf("user/123/%s", tt.label)}, + ).Return(tt.launchctlKill.First, tt.launchctlKill.Second).Once() + } - // Test #3: launchctl fails with expected error - t.Run("Exits without error if service not found", func(t *testing.T) { - testCommandExecutor.On("Run", - mock.AnythingOfType("string"), - mock.AnythingOfType("[]string"), - ).Return(daemon.LaunchdServiceNotFoundErrorCode, nil).Once() + // Call function + err := launchd.Stop(tt.label) + mock.AssertExpectationsForObjects(t, testCommandExecutor) + if tt.expectErr { + assert.NotNil(t, err) + } else { + assert.Nil(t, err) + } + }) - err := launchd.Stop(basicDaemonConfig.Label) - assert.Nil(t, err) - mock.AssertExpectationsForObjects(t, testCommandExecutor) - }) + // Reset the mocks between tests + testCommandExecutor.Mock = mock.Mock{} + } } var launchdRemoveTests = []struct { diff --git a/scripts/asciidoctor-extensions.rb b/scripts/asciidoctor-extensions.rb new file mode 100644 index 0000000..08eb4de --- /dev/null +++ b/scripts/asciidoctor-extensions.rb @@ -0,0 +1,50 @@ +require 'asciidoctor' +require 'asciidoctor/extensions' + +module GitBundleServer + module Documentation + class ManInlineMacro < Asciidoctor::Extensions::InlineMacroProcessor + use_dsl + + named :man + name_positional_attributes 'volnum' + + def process parent, target, attrs + suffix = (volnum = attrs['volnum']) ? %((#{volnum})) : '' + if parent.document.backend == 'manpage' + # If we're building a manpage, bold the page name + node = create_inline parent, :quoted, target, type: :strong + else + # Otherwise, leave the name as-provided + node = create_inline parent, :quoted, target + end + create_inline parent, :quoted, %(#{node.convert}#{suffix}) + end + end + + class UrlInlineMacro < Asciidoctor::Extensions::InlineMacroProcessor + use_dsl + + named :url + + def process parent, target, attrs + doc = parent.document + if doc.backend == 'manpage' + # If we're building a manpage, underline the name and escape the URL + # to avoid autolinking (the .URL that Asciidoc creates doesn't + # render correctly on all systems). + escape = target.start_with?( 'http://', 'https://', 'ftp://', 'irc://', 'mailto://') ? '\\' : '' + create_inline parent, :quoted, %(#{escape}#{target}), type: :emphasis + else + # Otherwise, pass through + create_inline parent, :quoted, target + end + end + end + end +end + +Asciidoctor::Extensions.register do + inline_macro GitBundleServer::Documentation::ManInlineMacro + inline_macro GitBundleServer::Documentation::UrlInlineMacro +end diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100755 index 0000000..9af48ac --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,124 @@ +#!/bin/bash +die () { + echo "$*" >&2 + exit 1 +} + +# Setup root escalation operation +SUDO="" +retry_root () { + if [ -n "$SUDO" ] + then + # run as user, then with 'sudo' + $@ 2>/dev/null || "$SUDO" $@ + else + # passthrough + $@ + fi +} + +# Parse script arguments +for i in "$@" +do +case "$i" in + --bindir=*) + BINDIR="${i#*=}" + shift # past argument=value + ;; + --docdir=*) + DOCDIR="${i#*=}" + shift # past argument=value + ;; + --uninstaller=*) + UNINSTALLER="${i#*=}" + shift # past argument=value + ;; + --allow-root) + SUDO=$(if command -v sudo >/dev/null 2>&1; then echo sudo; fi) + shift # past argument + ;; + --include-symlinks) + INCLUDE_SYMLINKS=1 + shift # past argument + ;; + --install-root=*) + INSTALL_ROOT="${i#*=}" + shift # past argument=value + ;; + *) + die "unknown option '$i'" + ;; +esac +done + +# Perform pre-execution checks +if [ -z "$BINDIR" ]; then + die "--bindir was not set" +fi +if [ -z "$DOCDIR" ]; then + die "--docdir was not set" +fi +if [ -z "$INSTALL_ROOT" ]; then + die "--install-root was not set" +fi + +if [ "$INSTALL_ROOT" == "/" ]; then + # Reset $INSTALL_ROOT to empty string to avoid double leading slash + INSTALL_ROOT="" +fi + +# Exit as soon as any line fails +set -e + +# Ensure payload directory exists +APP_ROOT="$INSTALL_ROOT/usr/local/git-bundle-server" +retry_root mkdir -p "$APP_ROOT" + +# Copy built binaries +echo "Copying binaries..." +retry_root cp -R "$BINDIR/." "$APP_ROOT/bin" + +echo "Copying manpages..." +for N in $(find "$DOCDIR" -type f | sed -e 's/.*\.//' | sort -u) +do + retry_root mkdir -p "$APP_ROOT/share/man/man$N" + retry_root cp -R "$DOCDIR/"*."$N" "$APP_ROOT/share/man/man$N" + +done + +# Copy uninstaller script +if [ -n "$UNINSTALLER" ]; then + echo "Copying uninstall script..." + retry_root cp "$UNINSTALLER" "$APP_ROOT" +fi + +# Create symlinks +if [ -n "$INCLUDE_SYMLINKS" ]; then + LINK_TO="$INSTALL_ROOT/usr/local/bin" + RELATIVE_LINK_TO_BIN="../git-bundle-server/bin" + retry_root mkdir -p "$LINK_TO" + + echo "Creating binary symlinks..." + for program in "$APP_ROOT"/bin/* + do + p=$(basename "$program") + retry_root rm -f "$LINK_TO/$p" + retry_root ln -s "$RELATIVE_LINK_TO_BIN/$p" "$LINK_TO/$p" + done + + echo "Creating manpage symlinks..." + for mandir in "$APP_ROOT"/share/man/man*/ + do + mdir=$(basename "$mandir") + LINK_TO="$INSTALL_ROOT/usr/local/share/man/$mdir" + RELATIVE_LINK_TO_MAN="../../../git-bundle-server/share/man/$mdir" + retry_root mkdir -p "$LINK_TO" + + for manpage in "$mandir"/* + do + mpage=$(basename "$manpage") + retry_root rm -f "$LINK_TO/$mpage" + retry_root ln -s "$RELATIVE_LINK_TO_MAN/$mpage" "$LINK_TO/$mpage" + done + done +fi diff --git a/scripts/make-docs.sh b/scripts/make-docs.sh new file mode 100755 index 0000000..a5977c4 --- /dev/null +++ b/scripts/make-docs.sh @@ -0,0 +1,48 @@ +#!/bin/bash +die () { + echo "$*" >&2 + exit 1 +} + +THISDIR="$( cd "$(dirname "$0")" ; pwd -P )" + +# Parse script arguments +for i in "$@" +do +case "$i" in + --docs=*) + MAN_DIR="${i#*=}" + shift # past argument=value + ;; + --output=*) + OUT_DIR="${i#*=}" + shift # past argument=value + ;; + *) + die "unknown option '$i'" + ;; +esac +done + +# Perform pre-execution checks +if [ -z "$MAN_DIR" ]; then + die "--docs was not set" +fi + +if [ -z "$OUT_DIR" ]; then + die "--output was not set" +fi + +set -e + +if ! command -v asciidoctor >/dev/null 2>&1 +then + die "cannot generate man pages: asciidoctor not installed. \ +See https://docs.asciidoctor.org/asciidoctor/latest/install for \ +installation instructions." +fi + +# Generate the man pages +mkdir -p "$OUT_DIR" + +asciidoctor -b manpage -I "$THISDIR" -r asciidoctor-extensions.rb -D "$OUT_DIR" "$MAN_DIR/*.adoc" diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh new file mode 100755 index 0000000..6b5d25b --- /dev/null +++ b/scripts/uninstall.sh @@ -0,0 +1,67 @@ +#!/bin/bash +set -e + +# Setup root escalation operation +SUDO=$(if command -v sudo >/dev/null 2>&1; then echo sudo; fi) +retry_root () { + $@ 2>/dev/null || "$SUDO" $@ +} + +THISDIR="$( cd "$(dirname "$0")" ; pwd -P )" +PATH_TO_BIN_SYMLINKS="$THISDIR/../bin" +PATH_TO_MAN_SYMLINKS="$THISDIR/../share/man" + +# Ensure we're running as root +if [ $(id -u) == "0" ] +then + echo + echo "WARNING: running this script as root will not remove user-scoped resources such" + echo "as daemon configurations." + echo + read -p "Are you sure you want to proceed? (y/N) " response + case $response in + [yY]*) + break # do nothing + ;; + [nN]*|"") + exit 0 # exit + ;; + *) + echo "Invalid response: $response" + ;; + esac +fi + +"$THISDIR/bin/git-bundle-server" web-server stop --remove + +# Remove symlinks +for program in "$THISDIR"/bin/* +do + symlink="$PATH_TO_BIN_SYMLINKS/$(basename $program)" + if [ -L "$symlink" ] + then + echo "Deleting '$symlink'..." + retry_root rm -f "$symlink" + else + echo "No symlink found at path '$symlink'." + fi +done + +# Remove application files +if [ -d "$THISDIR" ] +then + echo "Deleting application files in '$THISDIR'..." + retry_root rm -rf "$THISDIR" +else + echo "No application files found." +fi + +# If installed via MacOS .pkg, remove package receipt +PKG_ID=com.github.gitbundleserver +if command -v pkgutil >/dev/null 2>&1 && pkgutil --pkgs=$PKG_ID >/dev/null 2>&1 +then + # Must run as root + $SUDO pkgutil --forget $PKG_ID +fi + +exit 0