@@ -62,3 +62,108 @@ func (e *Executor) IsClean(ctx context.Context, dir string) (bool, error) {
6262 }
6363 return len (strings .TrimSpace (string (out ))) == 0 , nil
6464}
65+
66+ func (e * Executor ) Branch (ctx context.Context , dir string ) (string , error ) {
67+ // Try to get branch name first
68+ out , err := e .exec .RunDir (ctx , dir , e .gitPath , "branch" , "--show-current" )
69+ if err != nil {
70+ return "" , fmt .Errorf ("git branch: %w" , err )
71+ }
72+
73+ branch := strings .TrimSpace (string (out ))
74+ if branch != "" {
75+ return branch , nil
76+ }
77+
78+ // Empty branch name means detached HEAD - get short commit SHA
79+ out , err = e .exec .RunDir (ctx , dir , e .gitPath , "rev-parse" , "--short" , "HEAD" )
80+ if err != nil {
81+ return "" , fmt .Errorf ("git rev-parse: %w" , err )
82+ }
83+
84+ return strings .TrimSpace (string (out )), nil
85+ }
86+
87+ func (e * Executor ) DefaultBranch (ctx context.Context , dir string ) (string , error ) {
88+ // Get the default branch from origin's HEAD reference
89+ out , err := e .exec .RunDir (ctx , dir , e .gitPath , "symbolic-ref" , "refs/remotes/origin/HEAD" , "--short" )
90+ if err != nil {
91+ return "" , fmt .Errorf ("git symbolic-ref: %w" , err )
92+ }
93+
94+ // Output is "origin/main" or "origin/master", strip the "origin/" prefix
95+ branch := strings .TrimSpace (string (out ))
96+ branch = strings .TrimPrefix (branch , "origin/" )
97+
98+ return branch , nil
99+ }
100+
101+ func (e * Executor ) DiffStats (ctx context.Context , dir string ) (additions , deletions int , err error ) {
102+ // Get the default branch to compare against
103+ defaultBranch , err := e .DefaultBranch (ctx , dir )
104+ if err != nil {
105+ // Fallback to comparing against HEAD if we can't determine default branch
106+ defaultBranch = "HEAD"
107+ }
108+
109+ var out []byte
110+ if defaultBranch == "HEAD" {
111+ // Compare working directory against HEAD
112+ out , err = e .exec .RunDir (ctx , dir , e .gitPath , "diff" , "--shortstat" , "HEAD" )
113+ } else {
114+ // Compare current branch against default branch (e.g., main...HEAD)
115+ out , err = e .exec .RunDir (ctx , dir , e .gitPath , "diff" , "--shortstat" , defaultBranch + "...HEAD" )
116+ }
117+
118+ if err != nil {
119+ return 0 , 0 , fmt .Errorf ("git diff: %w" , err )
120+ }
121+
122+ return parseDiffStats (string (out ))
123+ }
124+
125+ // parseDiffStats parses git diff --shortstat output.
126+ // Example: " 3 files changed, 10 insertions(+), 5 deletions(-)"
127+ func parseDiffStats (output string ) (additions , deletions int , err error ) {
128+ output = strings .TrimSpace (output )
129+ if output == "" {
130+ return 0 , 0 , nil
131+ }
132+
133+ // Parse insertions
134+ if idx := strings .Index (output , "insertion" ); idx != - 1 {
135+ // Find the number before "insertion"
136+ start := strings .LastIndex (output [:idx ], "," )
137+ if start == - 1 {
138+ start = strings .LastIndex (output [:idx ], "changed" )
139+ }
140+ if start != - 1 {
141+ numStr := strings .TrimSpace (output [start + 1 : idx ])
142+ numStr = strings .Fields (numStr )[0 ]
143+ additions , _ = parseInt (numStr )
144+ }
145+ }
146+
147+ // Parse deletions
148+ if idx := strings .Index (output , "deletion" ); idx != - 1 {
149+ // Find the number before "deletion"
150+ start := strings .LastIndex (output [:idx ], "," )
151+ if start != - 1 {
152+ numStr := strings .TrimSpace (output [start + 1 : idx ])
153+ numStr = strings .Fields (numStr )[0 ]
154+ deletions , _ = parseInt (numStr )
155+ }
156+ }
157+
158+ return additions , deletions , nil
159+ }
160+
161+ func parseInt (s string ) (int , error ) {
162+ var n int
163+ for _ , c := range s {
164+ if c >= '0' && c <= '9' {
165+ n = n * 10 + int (c - '0' )
166+ }
167+ }
168+ return n , nil
169+ }
0 commit comments