-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathgit.go
More file actions
224 lines (196 loc) · 6.29 KB
/
git.go
File metadata and controls
224 lines (196 loc) · 6.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
package git
// This file just contains the Git-specific portions of sshd.
import (
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"text/template"
"github.com/Masterminds/cookoo"
"github.com/Masterminds/cookoo/log"
"golang.org/x/crypto/ssh"
)
// PrereceiveHookTpl is a pre-receive hook.
//
// This is overridable. The following template variables are passed into it:
//
// .GitHome: the path to Git's home directory.
var PrereceiveHookTpl = `#!/bin/bash
strip_remote_prefix() {
stdbuf -i0 -o0 -e0 sed "s/^/"$'\e[1G'"/"
}
while read oldrev newrev refname
do
LOCKFILE="/tmp/$RECEIVE_REPO.lock"
if ( set -o noclobber; echo "$$" > "$LOCKFILE" ) 2> /dev/null; then
trap 'rm -f "$LOCKFILE"; exit 1' INT TERM EXIT
# check for authorization on this repo
{{.GitHome}}/receiver "$RECEIVE_REPO" "$newrev" "$RECEIVE_USER" "$RECEIVE_FINGERPRINT"
rc=$?
if [[ $rc != 0 ]] ; then
echo " ERROR: failed on rev $newrev - push denied"
exit $rc
fi
# builder assumes that we are running this script from $GITHOME
cd {{.GitHome}}
# if we're processing a receive-pack on an existing repo, run a build
if [[ $SSH_ORIGINAL_COMMAND == git-receive-pack* ]]; then
{{.GitHome}}/builder "$RECEIVE_USER" "$RECEIVE_REPO" "$newrev" 2>&1 | strip_remote_prefix
fi
rm -f "$LOCKFILE"
trap - INT TERM EXIT
else
echo "Another git push is ongoing. Aborting..."
exit 1
fi
done
`
// Receive receives a Git repo.
// This will only work for git-receive-pack.
//
// Params:
// - operation (string): e.g. git-receive-pack
// - repoName (string): The repository name, in the form '/REPO.git'.
// - channel (ssh.Channel): The channel.
// - request (*ssh.Request): The channel.
// - gitHome (string): Defaults to /home/git.
// - fingerprint (string): The fingerprint of the user's SSH key.
// - user (string): The name of the Deis user.
//
// Returns:
// - nothing
func Receive(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) {
if ok, z := p.Requires("channel", "request", "fingerprint", "permissions"); !ok {
return nil, fmt.Errorf("Missing requirements %q", z)
}
repoName := p.Get("repoName", "").(string)
operation := p.Get("operation", "").(string)
channel := p.Get("channel", nil).(ssh.Channel)
gitHome := p.Get("gitHome", "/home/git").(string)
fingerprint := p.Get("fingerprint", nil).(string)
user := p.Get("user", "").(string)
repo, err := cleanRepoName(repoName)
if err != nil {
log.Warnf(c, "Illegal repo name: %s.", err)
channel.Stderr().Write([]byte("No repo given"))
return nil, err
}
repo += ".git"
if _, err := createRepo(c, filepath.Join(gitHome, repo), gitHome); err != nil {
log.Infof(c, "Did not create new repo: %s", err)
}
cmd := exec.Command("git-shell", "-c", fmt.Sprintf("%s '%s'", operation, repo))
log.Infof(c, strings.Join(cmd.Args, " "))
var errbuff bytes.Buffer
cmd.Dir = gitHome
cmd.Env = []string{
fmt.Sprintf("RECEIVE_USER=%s", user),
fmt.Sprintf("RECEIVE_REPO=%s", repo),
fmt.Sprintf("RECEIVE_FINGERPRINT=%s", fingerprint),
fmt.Sprintf("SSH_ORIGINAL_COMMAND=%s '%s'", operation, repo),
fmt.Sprintf("SSH_CONNECTION=%s", c.Get("SSH_CONNECTION", "0 0 0 0").(string)),
}
cmd.Env = append(cmd.Env, os.Environ()...)
done := plumbCommand(cmd, channel, &errbuff)
if err := cmd.Start(); err != nil {
log.Warnf(c, "Failed git receive immediately: %s %s", err, errbuff.Bytes())
return nil, err
}
fmt.Printf("Waiting for git-receive to run.\n")
done.Wait()
fmt.Printf("Waiting for deploy.\n")
if err := cmd.Wait(); err != nil {
log.Errf(c, "Error on command: %s %s", err, errbuff.Bytes())
return nil, err
}
if errbuff.Len() > 0 {
log.Warnf(c, "Unreported error: %s", errbuff.Bytes())
}
log.Infof(c, "Deploy complete.\n")
return nil, nil
}
func execAs(user, cmd string, args ...string) *exec.Cmd {
fullCmd := cmd + " " + strings.Join(args, " ")
return exec.Command("su", user, "-c", fullCmd)
}
// cleanRepoName cleans a repository name for a git-sh operation.
func cleanRepoName(name string) (string, error) {
if len(name) == 0 {
return name, errors.New("Empty repo name.")
}
if strings.Contains(name, "..") {
return "", errors.New("Cannot change directory in file name.")
}
name = strings.Replace(name, "'", "", -1)
return strings.TrimPrefix(strings.TrimSuffix(name, ".git"), "/"), nil
}
// plumbCommand connects the exec in/output and the channel in/output.
//
// The sidechannel is for sending errors to logs.
func plumbCommand(cmd *exec.Cmd, channel ssh.Channel, sidechannel io.Writer) *sync.WaitGroup {
var wg sync.WaitGroup
inpipe, _ := cmd.StdinPipe()
go func() {
io.Copy(inpipe, channel)
inpipe.Close()
}()
cmd.Stdout = channel
cmd.Stderr = channel.Stderr()
return &wg
}
var createLock sync.Mutex
// createRepo creates a new Git repo if it is not present already.
//
// Largely inspired by gitreceived from Flynn.
//
// Returns a bool indicating whether a project was created (true) or already
// existed (false).
func createRepo(c cookoo.Context, repoPath, gitHome string) (bool, error) {
createLock.Lock()
defer createLock.Unlock()
if fi, err := os.Stat(repoPath); err == nil && fi.IsDir() {
// Nothing to do.
log.Infof(c, "Directory %s already exists.", repoPath)
return false, nil
} else if os.IsNotExist(err) {
log.Infof(c, "Creating new directory at %s", repoPath)
// Create directory
if err := os.MkdirAll(repoPath, 0755); err != nil {
log.Warnf(c, "Failed to create repository: %s", err)
return false, err
}
cmd := exec.Command("git", "init", "--bare")
cmd.Dir = repoPath
if out, err := cmd.CombinedOutput(); err != nil {
log.Warnf(c, "git init output: %s", out)
return false, err
}
hook, err := prereceiveHook(map[string]string{"GitHome": gitHome})
if err != nil {
return true, err
}
ioutil.WriteFile(filepath.Join(repoPath, "hooks", "pre-receive"), hook, 0755)
return true, nil
} else if err == nil {
return false, errors.New("Expected directory, found file.")
} else {
return false, err
}
}
//prereceiveHook templates a pre-receive hook for Git.
func prereceiveHook(vars map[string]string) ([]byte, error) {
var out bytes.Buffer
// We parse the template anew each receive in case it has changed.
t, err := template.New("hooks").Parse(PrereceiveHookTpl)
if err != nil {
return []byte{}, err
}
err = t.Execute(&out, vars)
return out.Bytes(), err
}