df8862e09cd18ff743d20c05169962069f444f2d
[buildfarm-client.git] / EximBuild / SCM.pm
1 use strict;
2
3 use File::Find;
4
5 =comment
6
7 Copyright (c) 2003-2010, Andrew Dunstan
8
9 See accompanying License file for license details
10
11 =cut 
12
13 ##########################################################################
14 #
15 # SCM Class and subclasses for specific SCMs (currently CVS and git).
16 #
17 #########################################################################
18
19 package EximBuild::SCM;
20
21 use vars qw($VERSION); $VERSION = 'REL_0.1';
22
23 # factory function to return the right subclass
24 sub new
25 {
26     my $class = shift;
27     my $conf = shift;
28     my $target = shift || 'exim';
29     if (defined($conf->{scm}) &&  $conf->{scm} =~ /^git$/i)
30     {
31         $conf->{scm} = 'git';
32         return new EximBuild::SCM::Git $conf, $target;
33     }
34     #elsif ((defined($conf->{scm}) &&  $conf->{scm} =~ /^cvs$/i )
35     #    ||$conf->{csvrepo}
36     #    ||$conf->{cvsmethod})
37     #{
38     #    $conf->{scm} = 'cvs';
39     #    return new EximBuild::SCM::CVS $conf, $target;
40     #}
41     die "only Git currently supported";
42 }
43
44 # common routine use for copying the source, called by the
45 # SCM objects (directly, not as class methods)
46 sub copy_source
47 {
48     my $using_msvc = shift;
49     my $target = shift;
50     my $build_path = shift;
51
52     # annoyingly, there isn't a standard perl module to do a recursive copy
53     # and I don't want to require use of the non-standard File::Copy::Recursive
54     system("cp -r $target $build_path 2>&1");
55     my $status = $? >> 8;
56     die "copying directories: $status" if $status;
57
58 }
59
60 # required operations in each subclass:
61 # new()
62 # copy_source_required()
63 # copy_source()
64 # check_access()
65 # get_build_path()
66 # checkout()
67 # cleanup()
68 # find_changed()
69 # get_versions()
70 # log_id()
71
72 ##################################
73 #
74 # SCM for git
75 #
76 ##################################
77
78 package EximBuild::SCM::Git;
79
80 use File::Copy;
81 use Cwd;
82
83 sub new
84 {
85     my $class = shift;
86     my $conf = shift;
87     my $target = shift;
88     my $self = {};
89     $self->{gitrepo} = $conf->{scmrepo} ||
90                        "git://git.exim.org/exim.git";
91     $self->{reference} = $conf->{git_reference}
92       if defined($conf->{git_reference});
93     $self->{mirror} =(
94         $target eq 'exim'
95         ? "$conf->{build_root}/exim.git"
96         :"$conf->{build_root}/$target-exim.git"
97     )if $conf->{git_keep_mirror};
98     $self->{ignore_mirror_failure} = $conf->{git_ignore_mirror_failure};
99     $self->{target} = $target;
100     return bless $self, $class;
101 }
102
103 sub copy_source_required
104 {
105     my $self = shift;
106
107     # always copy git
108     return 1;
109 }
110
111 sub copy_source
112 {
113     my $self = shift;
114     my $using_msvc = shift;
115     my $target = $self->{target};
116     my $build_path = $self->{build_path};
117     die "no build path" unless $build_path;
118
119     # we don't want to copy the (very large) .git directory
120     # so we just move it out of the way during the copy
121     # there might be better ways of doing this, but this should do for now
122
123     move "$target/.git", "./git-save";
124     EximBuild::SCM::copy_source($using_msvc,$target,$build_path);
125     move "./git-save","$target/.git";
126 }
127
128 sub get_build_path
129 {
130     my $self = shift;
131     my $use_vpath = shift; # irrelevant for git
132     my $target = $self->{target};
133     $self->{build_path} = "$target.$$";
134     return      $self->{build_path};
135 }
136
137 sub check_access
138 {
139
140     # no login required?
141     return;
142 }
143
144 sub log_id
145 {
146     my $self = shift;
147     main::writelog('githead',[$self->{headref}])
148       if $self->{headref};
149 }
150
151 sub checkout
152 {
153
154     my $self = shift;
155     my $branch = shift;
156     my $gitserver = $self->{gitrepo};
157     my $target = $self->{target};
158     my $status;
159
160     # Msysgit does some horrible things, especially when it expects a drive
161     # spec and doesn't get one.  So we extract it if it exists and use it
162     # where necessary.
163     my $drive = "";
164     my $cwd = getcwd();
165     $drive = substr($cwd,0,2) if $cwd =~ /^[A-Z]:/;
166
167     my @gitlog;
168     if ($self->{mirror})
169     {
170
171         my $mirror = $target eq 'exim' ? 'exim.git' : "$target-exim.git";
172
173         if (-d $self->{mirror})
174         {
175             @gitlog = `git --git-dir="$self->{mirror}" fetch 2>&1`;
176             $status = $self->{ignore_mirror_failure} ? 0 : $? >> 8;
177         }
178         else
179         {
180             my $char1 = substr($gitserver,0,1);
181             $gitserver = "$drive$gitserver"
182               if ( $char1 eq '/' or $char1 eq '\\');
183
184             # this will fail on older git versions
185             # workaround is to do this manually in the buildroot:
186             #   git clone --bare $gitserver exim.git
187             #   (cd exim.git && git remote add --mirror origin $gitserver)
188             # or equivalent for other targets
189             @gitlog = `git clone --mirror $gitserver $self->{mirror} 2>&1`;
190             $status = $? >>8;
191         }
192         if ($status)
193         {
194             unshift(@gitlog,"Git mirror failure:\n");
195             print @gitlog if ($main::verbose);
196             main::send_result('Git-mirror',$status,\@gitlog);
197         }
198     }
199
200     push @gitlog, "Git arguments:\n".
201                   "  branch=$branch gitserver=$gitserver target=$target\n\n";
202
203     if (-d $target)
204     {
205         chdir $target;
206         my @branches = `git branch 2>&1`;
207         unless (grep {/^\* bf_$branch$/} @branches)
208         {
209             chdir '..';
210             print "Missing checked out branch bf_$branch:\n",@branches
211               if ($main::verbose);
212             unshift @branches,"Missing checked out branch bf_$branch:\n";
213             main::send_result("$target-Git",$status,\@branches);
214         }
215         my @pulllog = `git pull 2>&1`;
216         push(@gitlog,@pulllog);
217         chdir '..';
218     }
219     else
220     {
221         my $reference =
222           defined($self->{reference}) ?"--reference $self->{reference}" : "";
223
224         my $base = $self->{mirror} || $gitserver;
225
226         my $char1 = substr($base,0,1);
227         $base = "$drive$base"
228           if ( $char1 eq '/' or $char1 eq '\\');
229
230         my @clonelog = `git clone -q $reference $base $target 2>&1`;
231         push(@gitlog,@clonelog);
232         $status = $? >>8;
233         if (!$status)
234         {
235             chdir $target;
236
237             # make sure we don't name the new branch HEAD
238             # also, safer to checkout origin/master than origin/HEAD, I think
239             my $rbranch = $branch eq 'HEAD' ? 'master' : $branch;
240             my @colog =
241               `git checkout -b bf_$branch --track origin/$rbranch 2>&1`;
242             push(@gitlog,@colog);
243             chdir "..";
244         }
245     }
246     $status = $? >>8;
247     print "================== git log =====================\n",@gitlog
248       if ($main::verbose > 1);
249
250     # can't call writelog here because we call cleanlogs after the
251     # checkout stage, since we only clear out the logs if we find we need to
252     # do a build run.
253     # consequence - we don't save the git log if we don't do a run
254     # doesn't matter too much because if git fails we exit anyway.
255
256     # Don't call git clean here. If the user has left stuff lying around it
257     # might be important to them, so instead of blowing it away just bitch
258     # loudly.
259
260     chdir "$target";
261     my @gitstat = `git status --porcelain 2>&1`;
262     chdir "..";
263
264     my ($headref,$refhandle);
265     if (open($refhandle,"$target/.git/refs/heads/bf_$branch"))
266     {
267         $headref = <$refhandle>;
268         chomp $headref;
269         close($refhandle);
270         $self->{headref} = $headref;
271     }
272
273     main::send_result("$target-Git",$status,\@gitlog)   if ($status);
274     unless ($main::nosend && $main::nostatus)
275     {
276         push(@gitlog,"===========",@gitstat);
277         main::send_result("$target-Git-Dirty",99,\@gitlog)
278           if (@gitstat);
279     }
280
281     # if we were successful, however, we return the info so that
282     # we can put it in the newly cleaned logdir  later on.
283     return \@gitlog;
284 }
285
286 sub cleanup
287 {
288     my $self = shift;
289     my $target = $self->{target};
290     chdir $target;
291     system("git clean -dfxq");
292     chdir "..";
293 }
294
295 # private Class level routine for getting changed file data
296 sub parse_log
297 {
298     my $cmd = shift;
299     my @lines = `$cmd`;
300     chomp(@lines);
301     my $commit;
302     my $list = {};
303     foreach my $line (@lines)
304     {
305         next if $line =~ /^(Author:|Date:|\s)/;
306         next unless $line;
307         if ($line =~ /^commit ([0-9a-zA-Z]+)/)
308         {
309             $commit = $1;
310         }
311         else
312         {
313
314             # anything else should be a file name
315             $line =~ s/\s+$//; # make sure all trailing space is trimmed
316             $list->{$line} ||= $commit; # keep most recent commit
317         }
318     }
319     return $list;
320 }
321
322 sub find_changed
323 {
324     my $self = shift;
325     my $target = $self->{target};
326     my $current_snap = shift;
327     my $last_run_snap = shift;
328     my $last_success_snap = shift || 0;
329     my $changed_files = shift;
330     my $changed_since_success = shift;
331
332     my $cmd = qq{git --git-dir=$target/.git log -n 1 "--pretty=format:%ct"};
333     $$current_snap = `$cmd` +0;
334
335     # get the list of changed files and stash the commit data
336
337     if ($last_run_snap)
338     {
339         if ($last_success_snap > 0 && $last_success_snap < $last_run_snap)
340         {
341             $last_success_snap++;
342             my $lrsscmd ="git  --git-dir=$target/.git log --name-only "
343               ."--since=$last_success_snap --until=$last_run_snap";
344             $self->{changed_since_success} = parse_log($lrsscmd);
345         }
346         else
347         {
348             $self->{changed_since_success} = {};
349         }
350         $last_run_snap++;
351         my $lrscmd ="git  --git-dir=$target/.git log --name-only "
352           ."--since=$last_run_snap";
353         $self->{changed_since_last_run} = parse_log($lrscmd);
354         foreach my $file (keys %{$self->{changed_since_last_run}})
355         {
356             delete $self->{changed_since_success}->{$file};
357         }
358     }
359     else
360     {
361         $self->{changed_since_last_run} = {};
362     }
363
364     @$changed_files = sort keys %{$self->{changed_since_last_run}};
365     @$changed_since_success = sort keys %{$self->{changed_since_success}};
366 }
367
368 sub get_versions
369 {
370     my $self = shift;
371     my $flist = shift;
372     return unless @$flist;
373     my @repoversions;
374
375     # for git we have already collected and stashed the info, so we just
376     # extract it from the stash.
377
378     foreach my $file (@$flist)
379     {
380         if (exists $self->{changed_since_last_run}->{$file})
381         {
382             my $commit = $self->{changed_since_last_run}->{$file};
383             push(@repoversions,"$file $commit");
384         }
385         elsif (exists $self->{changed_since_success}->{$file})
386         {
387             my $commit = $self->{changed_since_success}->{$file};
388             push(@repoversions,"$file $commit");
389         }
390     }
391     @$flist = @repoversions;
392 }
393
394 1;