Welcome to mirror list, hosted at ThFree Co, Russian Federation.

updateref.go « updateref « git « internal - gitlab.com/gitlab-org/gitaly.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 315ba0c3f93f793213262300868860d11fb4d86a (plain)
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
package updateref

import (
	"bufio"
	"bytes"
	"context"
	"fmt"
	"regexp"

	"gitlab.com/gitlab-org/gitaly/v15/internal/command"
	"gitlab.com/gitlab-org/gitaly/v15/internal/git"
)

// ErrAlreadyLocked indicates a reference cannot be locked because another
// process has already locked it.
type ErrAlreadyLocked struct {
	Ref string
}

func (e *ErrAlreadyLocked) Error() string {
	return fmt.Sprintf("reference is already locked: %q", e.Ref)
}

// Updater wraps a `git update-ref --stdin` process, presenting an interface
// that allows references to be easily updated in bulk. It is not suitable for
// concurrent use.
type Updater struct {
	repo   git.RepositoryExecutor
	cmd    *command.Command
	stdout *bufio.Reader
	stderr *bytes.Buffer

	// withStatusFlushing determines whether the Git version used supports proper flushing of
	// status messages.
	withStatusFlushing bool
}

// UpdaterOpt is a type representing options for the Updater.
type UpdaterOpt func(*updaterConfig)

type updaterConfig struct {
	disableTransactions bool
}

// WithDisabledTransactions disables hooks such that no reference-transactions
// are used for the updater.
func WithDisabledTransactions() UpdaterOpt {
	return func(cfg *updaterConfig) {
		cfg.disableTransactions = true
	}
}

// New returns a new bulk updater, wrapping a `git update-ref` process. Call the
// various methods to enqueue updates, then call Commit() to attempt to apply all
// the updates at once.
//
// It is important that ctx gets canceled somewhere. If it doesn't, the process
// spawned by New() may never terminate.
func New(ctx context.Context, repo git.RepositoryExecutor, opts ...UpdaterOpt) (*Updater, error) {
	var cfg updaterConfig
	for _, opt := range opts {
		opt(&cfg)
	}

	txOption := git.WithRefTxHook(repo)
	if cfg.disableTransactions {
		txOption = git.WithDisabledHooks()
	}

	var stderr bytes.Buffer
	cmd, err := repo.Exec(ctx,
		git.SubCmd{
			Name:  "update-ref",
			Flags: []git.Option{git.Flag{Name: "-z"}, git.Flag{Name: "--stdin"}},
		},
		txOption,
		git.WithSetupStdin(),
		git.WithStderr(&stderr),
	)
	if err != nil {
		return nil, err
	}

	gitVersion, err := repo.GitVersion(ctx)
	if err != nil {
		return nil, fmt.Errorf("determining git version: %w", err)
	}

	updater := &Updater{
		repo:               repo,
		cmd:                cmd,
		stderr:             &stderr,
		stdout:             bufio.NewReader(cmd),
		withStatusFlushing: gitVersion.FlushesUpdaterefStatus(),
	}

	// By writing an explicit "start" to the command, we enable
	// transactional behaviour. Which effectively means that without an
	// explicit "commit", no changes will be inadvertently committed to
	// disk.
	if err := updater.setState("start"); err != nil {
		return nil, err
	}

	return updater, nil
}

// Update commands the reference to be updated to point at the object ID specified in newOID. If
// newOID is the zero OID, then the branch will be deleted. If oldOID is a non-empty string, then
// the reference will only be updated if its current value matches the old value. If the old value
// is the zero OID, then the branch must not exist.
func (u *Updater) Update(reference git.ReferenceName, newOID, oldOID git.ObjectID) error {
	_, err := fmt.Fprintf(u.cmd, "update %s\x00%s\x00%s\x00", reference.String(), newOID, oldOID)
	return err
}

// Create commands the reference to be created with the given object ID. The ref must not exist.
func (u *Updater) Create(reference git.ReferenceName, oid git.ObjectID) error {
	return u.Update(reference, oid, git.ObjectHashSHA1.ZeroOID)
}

// Delete commands the reference to be removed from the repository. This command will ignore any old
// state of the reference and just force-remove it.
func (u *Updater) Delete(reference git.ReferenceName) error {
	return u.Update(reference, git.ObjectHashSHA1.ZeroOID, "")
}

var refLockedRegex = regexp.MustCompile("cannot lock ref '(.+?)'")

// Prepare prepares the reference transaction by locking all references and determining their
// current values. The updates are not yet committed and will be rolled back in case there is no
// call to `Commit()`. This call is optional.
func (u *Updater) Prepare() error {
	if err := u.setState("prepare"); err != nil {
		matches := refLockedRegex.FindSubmatch([]byte(err.Error()))
		if len(matches) > 1 {
			return &ErrAlreadyLocked{Ref: string(matches[1])}
		}

		return err
	}

	return nil
}

// Commit applies the commands specified in other calls to the Updater
func (u *Updater) Commit() error {
	if err := u.setState("commit"); err != nil {
		return err
	}

	if err := u.cmd.Wait(); err != nil {
		return fmt.Errorf("git update-ref: %v, stderr: %q", err, u.stderr)
	}

	return nil
}

// Cancel aborts the transaction. No changes will be written to disk, all lockfiles will be cleaned
// up and the process will exit.
func (u *Updater) Cancel() error {
	if err := u.cmd.Wait(); err != nil {
		return fmt.Errorf("canceling update: %w", err)
	}
	return nil
}

func (u *Updater) setState(state string) error {
	_, err := fmt.Fprintf(u.cmd, "%s\x00", state)
	if err != nil {
		// We need to explicitly cancel the command here and wait for it to terminate such
		// that we can retrieve the command's stderr in a race-free manner.
		_ = u.Cancel()
		return fmt.Errorf("updating state to %q: %w, stderr: %q", state, err, u.stderr)
	}

	// For each state-changing command, git-update-ref(1) will report successful execution via
	// "<command>: ok" lines printed to its stdout. Ideally, we should thus verify here whether
	// the command was successfully executed by checking for exactly this line, otherwise we
	// cannot be sure whether the command has correctly been processed by Git or if an error was
	// raised. Unfortunately, Git only knows to flush these reports either starting with v2.34.0
	// or with our backported version v2.33.0.gl3.
	if u.withStatusFlushing {
		line, err := u.stdout.ReadString('\n')
		if err != nil {
			// We need to explicitly cancel the command here and wait for it to
			// terminate such that we can retrieve the command's stderr in a race-free
			// manner.
			_ = u.Cancel()

			return fmt.Errorf("state update to %q failed: %w, stderr: %q", state, err, u.stderr)
		}

		if line != fmt.Sprintf("%s: ok\n", state) {
			return fmt.Errorf("state update to %q not successful: expected ok, got %q", state, line)
		}
	}

	return nil
}