File: references.go

package info (click to toggle)
gittuf 0.12.0-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 3,692 kB
  • sloc: python: 85; makefile: 58; sh: 1
file content (248 lines) | stat: -rw-r--r-- 7,011 bytes parent folder | download | duplicates (2)
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
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
// Copyright The gittuf Authors
// SPDX-License-Identifier: Apache-2.0

package gitinterface

import (
	"errors"
	"fmt"
	"os"
	"path"
	"strings"
)

const (
	RefPrefix       = "refs/"
	BranchRefPrefix = "refs/heads/"
	TagRefPrefix    = "refs/tags/"
	RemoteRefPrefix = "refs/remotes/"
)

var (
	ErrReferenceNotFound = errors.New("requested Git reference not found")
)

// GetReference returns the tip of the specified Git reference.
func (r *Repository) GetReference(refName string) (Hash, error) {
	refTipID, err := r.executor("rev-parse", refName).executeString()
	if err != nil {
		if strings.Contains(err.Error(), "unknown revision or path not in the working tree") {
			return ZeroHash, ErrReferenceNotFound
		}
		return ZeroHash, fmt.Errorf("unable to read reference '%s': %w", refName, err)
	}

	hash, err := NewHash(refTipID)
	if err != nil {
		return ZeroHash, fmt.Errorf("invalid Git ID for reference '%s': %w", refName, err)
	}

	return hash, nil
}

// SetReference sets the specified reference to the provided Git ID.
func (r *Repository) SetReference(refName string, gitID Hash) error {
	_, err := r.executor("update-ref", "--create-reflog", refName, gitID.String()).executeString()
	if err != nil {
		return fmt.Errorf("unable to set Git reference '%s' to '%s': %w", refName, gitID.String(), err)
	}

	return nil
}

// DeleteReference deletes the specified Git reference.
func (r *Repository) DeleteReference(refName string) error {
	_, err := r.executor("update-ref", "-d", refName).executeString()
	if err != nil {
		return fmt.Errorf("unable to delete Git reference '%s': %w", refName, err)
	}
	return nil
}

// CheckAndSetReference sets the specified reference to the provided Git ID if
// the reference is currently set to `oldGitID`.
func (r *Repository) CheckAndSetReference(refName string, newGitID, oldGitID Hash) error {
	_, err := r.executor("update-ref", "--create-reflog", refName, newGitID.String(), oldGitID.String()).executeString()
	if err != nil {
		return fmt.Errorf("unable to set Git reference '%s' to '%s': %w", refName, newGitID.String(), err)
	}

	return nil
}

// GetSymbolicReferenceTarget returns the name of the Git reference the provided
// symbolic Git reference is pointing to.
func (r *Repository) GetSymbolicReferenceTarget(refName string) (string, error) {
	symTarget, err := r.executor("symbolic-ref", refName).executeString()
	if err != nil {
		return "", fmt.Errorf("unable to resolve %s: %w", refName, err)
	}

	return symTarget, nil
}

// SetSymbolicReference sets the specified symbolic reference to the specified
// target reference.
func (r *Repository) SetSymbolicReference(symRefName, targetRefName string) error {
	_, err := r.executor("symbolic-ref", symRefName, targetRefName).executeString()
	if err != nil {
		return fmt.Errorf("unable to set symbolic Git reference '%s' to '%s': %w", symRefName, targetRefName, err)
	}

	return nil
}

// AbsoluteReference returns the fully qualified reference path for the provided
// Git ref.
// Source: https://git-scm.com/docs/gitrevisions#Documentation/gitrevisions.txt-emltrefnamegtemegemmasterememheadsmasterememrefsheadsmasterem
func (r *Repository) AbsoluteReference(target string) (string, error) {
	_, err := os.Stat(path.Join(r.gitDirPath, target))
	if err == nil {
		if strings.HasPrefix(target, RefPrefix) {
			// not symbolic ref
			return target, nil
		}
		// symbolic ref such as .git/HEAD
		return r.GetSymbolicReferenceTarget(target)
	}

	// We may have a ref that isn't available locally but is still ref-prefixed.
	if strings.HasPrefix(target, RefPrefix) {
		return target, nil
	}

	// If target is a full ref already and it's stored in the GIT_DIR/refs
	// directory, we don't reach this point. Below, we handle cases where the
	// ref may be packed.

	// Check if custom reference
	customName := CustomReferenceName(target)
	_, err = r.GetReference(customName)
	if err == nil {
		return customName, nil
	}
	if !errors.Is(err, ErrReferenceNotFound) {
		return "", err
	}

	// Check if tag
	tagName := TagReferenceName(target)
	_, err = r.GetReference(tagName)
	if err == nil {
		return tagName, nil
	}
	if !errors.Is(err, ErrReferenceNotFound) {
		return "", err
	}

	// Check if branch
	branchName := BranchReferenceName(target)
	_, err = r.GetReference(branchName)
	if err == nil {
		return branchName, nil
	}
	if !errors.Is(err, ErrReferenceNotFound) {
		return "", err
	}

	// Check if remote tracker ref
	remoteRefName := RemoteReferenceName(target)
	_, err = r.GetReference(remoteRefName)
	if err == nil {
		return branchName, nil
	}
	if !errors.Is(err, ErrReferenceNotFound) {
		return "", err
	}

	remoteRefHEAD := path.Join(remoteRefName, "HEAD")
	_, err = r.GetReference(remoteRefHEAD)
	if err == nil {
		return branchName, nil
	}
	if !errors.Is(err, ErrReferenceNotFound) {
		return "", err
	}

	return "", ErrReferenceNotFound
}

// RefSpec creates a Git refspec for the specified ref.  For more information on
// the Git refspec, please consult:
// https://git-scm.com/book/en/v2/Git-Internals-The-Refspec.
func (r *Repository) RefSpec(refName, remoteName string, fastForwardOnly bool) (string, error) {
	var (
		refPath string
		err     error
	)

	refPath = refName
	if !strings.HasPrefix(refPath, RefPrefix) {
		refPath, err = r.AbsoluteReference(refName)
		if err != nil {
			return "", err
		}
	}

	if strings.HasPrefix(refPath, TagRefPrefix) {
		// TODO: check if this is correct, AFAICT tags aren't tracked in the
		// remotes namespace.
		fastForwardOnly = true
	}

	// local is always refPath, destination depends on remoteName
	localPath := refPath
	var remotePath string
	if len(remoteName) > 0 {
		remotePath = RemoteRef(refPath, remoteName)
	} else {
		remotePath = refPath
	}

	refSpecString := fmt.Sprintf("%s:%s", localPath, remotePath)
	if !fastForwardOnly {
		refSpecString = fmt.Sprintf("+%s", refSpecString)
	}

	return refSpecString, nil
}

// CustomReferenceName returns the full reference name in the form
// `refs/<customName>`.
func CustomReferenceName(customName string) string {
	if strings.HasPrefix(customName, RefPrefix) {
		return customName
	}

	return fmt.Sprintf("%s%s", RefPrefix, customName)
}

// TagReferenceName returns the full reference name for the specified tag in the
// form `refs/tags/<tagName>`.
func TagReferenceName(tagName string) string {
	if strings.HasPrefix(tagName, TagRefPrefix) {
		return tagName
	}

	return fmt.Sprintf("%s%s", TagRefPrefix, tagName)
}

// BranchReferenceName returns the full reference name for the specified branch
// in the form `refs/heads/<branchName>`.
func BranchReferenceName(branchName string) string {
	if strings.HasPrefix(branchName, BranchRefPrefix) {
		return branchName
	}

	return fmt.Sprintf("%s%s", BranchRefPrefix, branchName)
}

// RemoteReferenceName returns the full reference name in the form
// `refs/remotes/<name>`.
func RemoteReferenceName(name string) string {
	if strings.HasPrefix(name, RemoteRefPrefix) {
		return name
	}

	return fmt.Sprintf("%s%s", RemoteRefPrefix, name)
}