3
3
//! Git related types and implementations for pulling/cloning source repos.
4
4
5
5
use crate :: {
6
- error:: { Context , Error as HcError , Result as HcResult } ,
6
+ error:: { Error as HcError , Result as HcResult } ,
7
7
hc_error,
8
- shell:: { progress_phase:: ProgressPhase , verbosity:: Verbosity , Shell } ,
9
8
} ;
10
- use console:: Term ;
11
- use git2:: {
12
- build:: { CheckoutBuilder , RepoBuilder } ,
13
- AnnotatedCommit , Branch , FetchOptions , Progress , Reference , RemoteCallbacks , Repository ,
14
- } ;
15
- use std:: { cell:: OnceCell , io:: Write , path:: Path } ;
16
9
use gix:: { bstr:: ByteSlice , refs:: FullName , remote, ObjectId } ;
17
10
use std:: path:: Path ;
18
11
use url:: Url ;
19
12
20
- /// Construct the remote callbacks object uesd when making callinging into [git2].
21
- fn make_remote_callbacks( ) -> RemoteCallbacks < ' static > {
22
- // Create progress phases for recieving the objects and resolving deltas.
23
- let transfer_phase: OnceCell < ProgressPhase > = OnceCell :: new ( ) ;
24
- let resolution_phase: OnceCell < ProgressPhase > = OnceCell :: new ( ) ;
25
-
26
- // Create a struct to hold the callbacks.
27
- let mut callbacks = RemoteCallbacks :: new ( ) ;
28
-
29
- // Messages from the remote ("Counting objects" etc) are sent over the sideband.
30
- // This involves clearing and replacing the line -- use console to do this effectively.
31
-
32
- match Shell :: get_verbosity ( ) {
33
- Verbosity :: Normal => {
34
- callbacks. sideband_progress ( move |msg : & [ u8 ] | {
35
- Shell :: in_suspend ( || {
36
- // use the standard output.
37
- let mut term = Term :: stdout ( ) ;
38
-
39
- // Crash on errors here, since they should be relatively uncommon.
40
- term. clear_line ( ) . expect ( "clear line on standard output" ) ;
41
-
42
- write ! ( & mut term, "remote: {}" , String :: from_utf8_lossy( msg) )
43
- . expect ( "wrote to standard output" ) ;
44
-
45
- term. flush ( ) . expect ( "flushed standard output" ) ;
46
- } ) ;
47
-
48
- true
49
- } ) ;
50
- }
51
- Verbosity :: Quiet | Verbosity :: Silent => { }
52
- }
53
-
54
- callbacks. transfer_progress ( move |prog : Progress | {
55
- if prog. received_objects ( ) > 0 {
56
- let phase = transfer_phase. get_or_init ( || {
57
- ProgressPhase :: start ( prog. total_objects ( ) as u64 , "(git) receiving objects" )
58
- } ) ;
59
-
60
- phase. set_position ( prog. received_objects ( ) as u64 ) ;
61
-
62
- if prog. received_objects ( ) == prog. total_objects ( ) && !phase. is_finished ( ) {
63
- phase. finish_successful ( false ) ;
64
- }
65
- }
66
-
67
- if prog. indexed_deltas ( ) > 0 {
68
- let phase = resolution_phase. get_or_init ( || {
69
- ProgressPhase :: start ( prog. total_deltas ( ) as u64 , "(git) resolving deltas" )
70
- } ) ;
71
-
72
- phase. set_position ( prog. indexed_deltas ( ) as u64 ) ;
73
-
74
- if prog. indexed_deltas ( ) == prog. total_deltas ( ) && !phase. is_finished ( ) {
75
- phase. finish_successful ( false ) ;
76
- }
77
- }
78
-
79
- true
80
- } ) ;
81
-
82
- callbacks
83
13
/// default options to use when fetching a repo with `gix`
84
14
fn fetch_options ( url : & Url , dest : & Path ) -> gix:: clone:: PrepareFetch {
85
15
gix:: clone:: PrepareFetch :: new (
@@ -92,35 +22,53 @@ fn fetch_options(url: &Url, dest: &Path) -> gix::clone::PrepareFetch {
92
22
. expect ( "fetch options must be valid to perform a clone" )
93
23
}
94
24
95
-
96
- fn make_checkout_builder ( ) -> CheckoutBuilder < ' static > {
97
- // Create a struct to hold callbacks while doing git checkout.
98
- let mut checkout_opts = CheckoutBuilder :: new ( ) ;
99
-
100
- // Make a phase to track the checkout progress.
101
- let checkout_phase: OnceCell < ProgressPhase > = OnceCell :: new ( ) ;
102
-
103
- // We don't care about the path being resolved, only the total and current numbers.
104
- checkout_opts. progress ( move |path, current, total| {
105
- // Initialize the phase if we haven't already.
106
- let phase =
107
- checkout_phase. get_or_init ( || ProgressPhase :: start ( total as u64 , "(git) checkout" ) ) ;
108
-
109
- // Set the bar to have the amount of progress in resolving.
110
- phase. set_position ( current as u64 ) ;
111
- // Set the progress bar's status to the path being resolved.
112
- phase. update_status (
113
- path. map ( Path :: to_string_lossy)
114
- . unwrap_or ( "resolving..." . into ( ) ) ,
115
- ) ;
116
-
117
- // If we have resolved everything, finish the phase.
118
- if current == total {
119
- phase. finish_successful ( false ) ;
120
- }
121
- } ) ;
122
-
123
- checkout_opts
25
+ /// fast-forward HEAD of current repo to a new_commit
26
+ ///
27
+ /// returns new ObjectId (hash) of updated HEAD upon success
28
+ fn fast_forward_to_hash (
29
+ repo : & gix:: Repository ,
30
+ current_head : gix:: Head ,
31
+ new_object_id : gix:: ObjectId ,
32
+ ) -> HcResult < ObjectId > {
33
+ let current_id = current_head
34
+ . id ( )
35
+ . ok_or_else ( || hc_error ! ( "Could not determine hash of current HEAD" ) ) ?;
36
+
37
+ if current_id == new_object_id {
38
+ log:: debug!( "skipping fast-forward, IDs match" ) ;
39
+ return Ok ( current_id. into ( ) ) ;
40
+ }
41
+ let edit = gix:: refs:: transaction:: RefEdit {
42
+ change : gix:: refs:: transaction:: Change :: Update {
43
+ log : gix:: refs:: transaction:: LogChange {
44
+ mode : gix:: refs:: transaction:: RefLog :: AndReference ,
45
+ force_create_reflog : false ,
46
+ message : format ! ( "fast-forward HEAD from {} to {}" , current_id, new_object_id)
47
+ . into ( ) ,
48
+ } ,
49
+ expected : gix:: refs:: transaction:: PreviousValue :: Any ,
50
+ new : gix:: refs:: Target :: Object ( new_object_id) ,
51
+ } ,
52
+ name : FullName :: try_from ( "HEAD" ) . unwrap ( ) ,
53
+ deref : true ,
54
+ } ;
55
+ log:: trace!(
56
+ "attempting fast-forward from {} to {}" ,
57
+ current_id,
58
+ new_object_id
59
+ ) ;
60
+
61
+ // commit change to the repo and the reflog
62
+ repo. refs
63
+ . transaction ( )
64
+ . prepare (
65
+ [ edit] ,
66
+ gix:: lock:: acquire:: Fail :: Immediately ,
67
+ gix:: lock:: acquire:: Fail :: Immediately ,
68
+ ) ?
69
+ . commit ( Some ( Default :: default ( ) ) ) ?;
70
+ log:: trace!( "fast-forward successful" ) ;
71
+ Ok ( new_object_id)
124
72
}
125
73
126
74
/// Clone a repo from the given url to a destination path in the filesystem.
@@ -135,113 +83,65 @@ pub fn clone(url: &Url, dest: &Path) -> HcResult<()> {
135
83
Ok ( ( ) )
136
84
}
137
85
138
- /// For a given repo, checkout a particular ref in a detached HEAD state. If no
139
- /// ref is provided, instead try to resolve the most correct ref to target. If
140
- /// the repo has one branch, try fast-forwarding to match upstream, then set HEAD
141
- /// to top of branch. Else, if the repo has one remote, try to find a local branch
142
- /// tracking the default branch of remote and set HEAD to that. Otherwise, error.
143
- pub fn checkout ( repo_path : & Path , refspec : Option < String > ) -> HcResult < String > {
144
- // Open the repo with git2.
145
- let repo : Repository = Repository :: open ( repo_path ) ? ;
146
- // Get the repo's head.
147
- let head: Reference = repo. head ( ) ?;
148
- // Get the shortname for later debugging.
149
- let init_short_name = head
150
- . shorthand ( )
151
- . ok_or ( HcError :: msg ( "HEAD shorthand should be UTF-8" ) ) ? ;
152
- let ret_str : String ;
153
- if let Some ( refspec_str ) = refspec {
154
- // Parse refspec as an annotated commit, and set HEAD based on that
155
-
156
- // Try refspec as given
157
- let tgt_ref : AnnotatedCommit = match repo . revparse_single ( & refspec_str ) {
158
- Ok ( object ) => repo. find_annotated_commit ( object . peel_to_commit ( ) ?. id ( ) ) ? ,
159
- // If that refspec is not found, try it again with a leading "v"
86
+ /// For a given repo, checkout a particular ref in a detached HEAD state.
87
+ ///
88
+ /// 1. If a refspec is passed, then attempt to fast-forward to the specified revision
89
+ /// 2. If no ref is provided, then attempt to fast-forward repo to HEAD of the default remote.
90
+ /// 3. If there is no default remote, then attempt to set HEAD to match upstream of local branch
91
+ ///
92
+ /// If none of these are possible, then error due to inability to infer target
93
+ pub fn checkout ( repo_path : & Path , refspec : Option < String > ) -> HcResult < gix :: ObjectId > {
94
+ let repo = gix :: open ( repo_path ) ? ;
95
+ let head = repo. head ( ) ?;
96
+
97
+ // if a refspec was given attempt to resolve it, error if unable to resolve
98
+ if let Some ( refspec ) = refspec {
99
+ log :: trace! ( "attempting to find refspec [{}]" , refspec ) ;
100
+ // try refspec as given
101
+ let target = match repo . rev_parse ( refspec. as_str ( ) ) {
102
+ Ok ( rev ) => {
103
+ let oid = rev
104
+ . single ( )
105
+ . ok_or_else ( || hc_error ! ( "ref '{}' was not a unique identifier" , refspec ) ) ? ;
106
+ repo. find_object ( oid ) ?. id
107
+ }
160
108
Err ( e) => {
161
109
return Err ( hc_error ! (
162
110
"Could not find repo with provided refspec: {}" ,
163
111
e
164
112
) ) ;
165
113
}
166
114
} ;
115
+ log:: trace!( "found refspec: {:?}" , target) ;
116
+ return fast_forward_to_hash ( & repo, head, target) ;
117
+ }
167
118
168
- repo. set_head_detached_from_annotated ( tgt_ref) ?;
169
- ret_str = refspec_str;
170
- } else {
171
- // Get names of remotes
172
- let raw_remotes = repo. remotes ( ) ?;
173
- let remotes = raw_remotes. into_iter ( ) . flatten ( ) . collect :: < Vec < & str > > ( ) ;
174
- let mut local_branches = repo
175
- . branches ( Some ( git2:: BranchType :: Local ) ) ?
176
- . filter_map ( |x| match x {
177
- Ok ( ( b, _) ) => Some ( b) ,
178
- _ => None ,
179
- } )
180
- . collect :: < Vec < Branch > > ( ) ;
181
- if local_branches. len ( ) == 1 {
182
- let mut local_branch = local_branches. remove ( 0 ) ;
183
- // if applicable, update local_branch reference to match newest remote commit
184
- if let Ok ( upstr) = local_branch. upstream ( ) {
185
- let remote_ref = upstr. into_reference ( ) ;
186
- let target_commit = repo
187
- . reference_to_annotated_commit ( & remote_ref)
188
- . context ( "Error creating annotated commit" ) ?;
189
- let reflog_msg = format ! (
190
- "Fast-forward {init_short_name} to id: {}" ,
191
- target_commit. id( )
192
- ) ;
193
- // Set the local branch to the given commit
194
- local_branch
195
- . get_mut ( )
196
- . set_target ( target_commit. id ( ) , & reflog_msg) ?;
197
- }
198
- // Get branch name in form "refs/heads/<NAME>"
199
- let tgt_ref = local_branch. get ( ) ;
200
- let local_name = tgt_ref. name ( ) . unwrap ( ) ;
201
- repo. set_head ( local_name) ?;
202
- ret_str = tgt_ref. shorthand ( ) . unwrap_or ( local_name) . to_owned ( ) ;
203
- } else if remotes. len ( ) == 1 {
204
- // Get name of default branch for remote
205
- let mut remote = repo. find_remote ( remotes. first ( ) . unwrap ( ) ) ?;
206
- remote. connect ( git2:: Direction :: Fetch ) ?;
207
- let default = remote. default_branch ( ) ?;
208
- // Get the <NAME> in "refs/heads/<NAME>" for remote
209
- let default_name = default. as_str ( ) . unwrap ( ) ;
210
- let ( _, remote_branch_name) = default_name. rsplit_once ( '/' ) . unwrap ( ) ;
211
- // Check if any local branches are tracking it
212
- let mut opt_tgt_head: Option < & str > = None ;
213
- for branch in local_branches. iter ( ) {
214
- let Ok ( upstr) = branch. upstream ( ) else {
215
- continue ;
216
- } ;
217
- // Get the <NAME> in "refs/remote/<REMOTE>/<NAME>"
218
- let upstream_name = upstr. get ( ) . name ( ) . unwrap ( ) ;
219
- let ( _, upstream_branch_name) = upstream_name. rsplit_once ( '/' ) . unwrap ( ) ;
220
- // If the branch names match, we have found our branch
221
- if upstream_branch_name == remote_branch_name {
222
- opt_tgt_head = Some ( branch. get ( ) . name ( ) . unwrap ( ) ) ;
223
- break ;
224
- }
119
+ // Determine if there is a default remote, if there is determine what it thinks HEAD is and
120
+ // fast-forward to the remote HEAD
121
+ if let Some ( Ok ( default_remote) ) = repo. find_default_remote ( remote:: Direction :: Fetch ) {
122
+ if let Some ( remote_name) = default_remote. name ( ) {
123
+ if let Ok ( mut remote_head) =
124
+ repo. find_reference ( format ! ( "refs/remotes/{}/HEAD" , remote_name. as_bstr( ) ) . as_str ( ) )
125
+ {
126
+ let target = remote_head. peel_to_id_in_place ( ) ?;
127
+ return fast_forward_to_hash ( & repo, head, target. into ( ) ) ;
225
128
}
226
- let Some ( local_name) = opt_tgt_head else {
227
- return Err ( HcError :: msg (
228
- "could not find local branch tracking remote default" ,
229
- ) ) ;
230
- } ;
231
- repo. set_head ( local_name) ?;
232
- let head_ref = repo. head ( ) ?;
233
- ret_str = head_ref. shorthand ( ) . unwrap_or ( local_name) . to_owned ( ) ;
234
- } else {
235
- return Err ( HcError :: msg (
236
- "repo has multiple local branches and remotes, target is ambiguous" ,
237
- ) ) ;
238
129
}
239
130
}
240
- repo. checkout_head ( Some ( make_checkout_builder ( ) . force ( ) ) ) ?;
241
131
242
- Ok ( ret_str)
132
+ let mut local_branches = repo. branch_names ( ) ;
133
+ if local_branches. len ( ) == 1 {
134
+ let mut local_branch = repo. find_reference (
135
+ format ! ( "refs/heads/{}" , local_branches. pop_first( ) . unwrap( ) ) . as_str ( ) ,
136
+ ) ?;
137
+ let tip_of_local_branch = local_branch. peel_to_id_in_place ( ) ?;
138
+ return fast_forward_to_hash ( & repo, head, tip_of_local_branch. into ( ) ) ;
139
+ }
140
+ Err ( HcError :: msg ( "target is ambiguous" ) )
243
141
}
244
142
143
+ /// TODO: redo commit history to add support for fetch/clone/checkout separately
144
+ /// TODO: add support for visual progress indicators
245
145
/// Perform a `git fetch` for all remotes in the repo.
246
146
pub fn fetch ( repo_path : & Path ) -> HcResult < ( ) > {
247
147
log:: debug!( "Fetching: {:?}" , repo_path) ;
0 commit comments