@@ -24,19 +24,20 @@ import (
2424 "mvdan.cc/sh/v3/syntax"
2525)
2626
27- const shellUsage = "usage: sh <repo> [branch]"
27+ const shellUsage = "usage: sh <repo> [branch, tag, or commit ]"
2828
2929// errShellInterrupt is what shellLines returns when Ctrl-C is pressed at the
3030// prompt, so the parser drops the pending input.
3131var errShellInterrupt = errors .New ("interrupt" )
3232
33- // shellTarget resolves the commit an sh session inspects: the tip of branch, or
34- // of HEAD's branch when branch is empty. The returned update describes that
35- // commit as if it had just been pushed: Old is its first parent, or the zero
36- // hash for a root commit.
37- func shellTarget (st storage.Storer , branch string ) (refUpdate , * object.Tree , error ) {
38- name := plumbing .NewBranchReferenceName (branch )
39- if branch == "" {
33+ // shellTarget resolves the commit an sh session inspects: a branch, tag, or
34+ // commit hash, or the tip of HEAD's branch when revision is empty. The
35+ // returned update describes that commit as if it had just been pushed: Old is
36+ // its first parent, or the zero hash for a root commit.
37+ func shellTarget (st storage.Storer , revision string ) (refUpdate , * object.Tree , error ) {
38+ var name plumbing.ReferenceName
39+ var hash plumbing.Hash
40+ if revision == "" {
4041 head , err := st .Reference (plumbing .HEAD )
4142 if err != nil {
4243 return refUpdate {}, nil , fmt .Errorf ("reading HEAD: %w" , err )
@@ -45,19 +46,45 @@ func shellTarget(st storage.Storer, branch string) (refUpdate, *object.Tree, err
4546 return refUpdate {}, nil , errors .New ("HEAD is detached; name a branch: " + shellUsage )
4647 }
4748 name = head .Target ()
49+ } else if strings .HasPrefix (revision , "refs/" ) {
50+ name = plumbing .ReferenceName (revision )
51+ } else {
52+ for _ , candidate := range []plumbing.ReferenceName {
53+ plumbing .NewBranchReferenceName (revision ),
54+ plumbing .NewTagReferenceName (revision ),
55+ } {
56+ ref , err := st .Reference (candidate )
57+ if err == nil {
58+ name , hash = ref .Name (), ref .Hash ()
59+ break
60+ }
61+ if ! errors .Is (err , plumbing .ErrReferenceNotFound ) {
62+ return refUpdate {}, nil , fmt .Errorf ("reading %s: %w" , candidate , err )
63+ }
64+ }
65+ if hash .IsZero () {
66+ if ! plumbing .IsHash (revision ) {
67+ return refUpdate {}, nil , fmt .Errorf ("branch %q not found" , revision )
68+ }
69+ hash = plumbing .NewHash (revision )
70+ name = plumbing .ReferenceName ("refs/commits/" + revision )
71+ }
4872 }
4973
50- ref , err := st .Reference (name )
51- if errors .Is (err , plumbing .ErrReferenceNotFound ) {
52- return refUpdate {}, nil , fmt .Errorf ("branch %q not found" , name .Short ())
53- }
54- if err != nil {
55- return refUpdate {}, nil , fmt .Errorf ("reading %s: %w" , name , err )
74+ if hash .IsZero () {
75+ ref , err := st .Reference (name )
76+ if errors .Is (err , plumbing .ErrReferenceNotFound ) {
77+ return refUpdate {}, nil , fmt .Errorf ("revision %q not found" , revision )
78+ }
79+ if err != nil {
80+ return refUpdate {}, nil , fmt .Errorf ("reading %s: %w" , name , err )
81+ }
82+ hash = ref .Hash ()
5683 }
5784
58- commit , err := object . GetCommit (st , ref . Hash () )
85+ commit , err := shellCommit (st , hash )
5986 if err != nil {
60- return refUpdate {}, nil , fmt .Errorf ("loading commit %s: %w" , ref . Hash () , err )
87+ return refUpdate {}, nil , fmt .Errorf ("loading commit %s: %w" , hash , err )
6188 }
6289 tree , err := commit .Tree ()
6390 if err != nil {
@@ -71,9 +98,34 @@ func shellTarget(st storage.Storer, branch string) (refUpdate, *object.Tree, err
7198 return u , tree , nil
7299}
73100
74- // handleShell services "sh <repo> [branch]": an interactive kefka shell with
101+ // shellCommit peels annotated tags until it reaches a commit. Git tags can
102+ // point to other tags or to non-commit objects, neither of which is a usable
103+ // shell checkout.
104+ func shellCommit (st storage.Storer , hash plumbing.Hash ) (* object.Commit , error ) {
105+ for range 16 {
106+ obj , err := st .EncodedObject (plumbing .AnyObject , hash )
107+ if err != nil {
108+ return nil , err
109+ }
110+ switch obj .Type () {
111+ case plumbing .CommitObject :
112+ return object .DecodeCommit (st , obj )
113+ case plumbing .TagObject :
114+ tag , err := object .DecodeTag (st , obj )
115+ if err != nil {
116+ return nil , err
117+ }
118+ hash = tag .Target
119+ default :
120+ return nil , fmt .Errorf ("object %s is not a commit or tag" , hash )
121+ }
122+ }
123+ return nil , errors .New ("tag chain is too deep" )
124+ }
125+
126+ // handleShell services "sh <repo> [revision]": an interactive kefka shell with
75127// the sandbox and environment a receive-pack hook gets, filled in from the
76- // branch's tip commit. It needs -allow-hooks, a PTY, and write access to the
128+ // selected revision's commit. It needs -allow-hooks, a PTY, and write access to the
77129// repository, because only a pusher can trigger a hook.
78130func (d * daemon ) handleShell (s ssh.Session , args []string ) {
79131 fail := func (format string , a ... any ) {
@@ -100,9 +152,9 @@ func (d *daemon) handleShell(s ssh.Session, args []string) {
100152 fail ("%v" , err )
101153 return
102154 }
103- var branch string
155+ var revision string
104156 if len (args ) == 2 {
105- branch = args [1 ]
157+ revision = args [1 ]
106158 }
107159
108160 var cred auth.Credential = auth.Anonymous {}
@@ -132,7 +184,7 @@ func (d *daemon) handleShell(s ssh.Session, args []string) {
132184 fail ("repository %q not found" , ref .Path ())
133185 return
134186 }
135- u , tree , err := shellTarget (st , branch )
187+ u , tree , err := shellTarget (st , revision )
136188 if err != nil {
137189 metrics .ObserveGitOp ("ssh" , "sh" , "error" , start )
138190 fail ("%v" , err )
0 commit comments